背景
现有系统是通过http与后端接口交互且使用的是对称秘钥加密用户密码且服务端校验密码通过后也是通过对称密码加密Token(用来客户端签名请求数据)返回给Web前端或APP端,由于秘钥保存在web前端或APP端那都可以通过阅读代码直接获取,故间接暴露用户密码与Token。
思路
- 方案一:将对称密码改成非对称密码,web端与app端写死公钥,并通过公钥加密用户密码并在客户端生成Token也加密传给服务端。
- 可保证请求数据的完整与保密,但无法保证响应数据的真实与保密(比如使用Fiddler就可以实现请求拦截并修改响应数据)
- 变更公私钥时需要同步变更前端代码,且无法实现版本兼容
- 只加密请求时的重要数据,请求耗时短
- 方案二:使用HTTPS
- 可保证服务端的真实性(不能被假冒,除非客户端信任了非CA颁发的恶意证书)
- 可保证客户端的真实性(同一个会话中不会出现不同客户端)
- 可保证数据内容的不被篡改与泄露
- 请求与响应的数据都被加密,请求耗时长
其实方案一与方案二都有各种的优劣,可以根据系统特性来选择。由于咱们系统会涉及到资金,就选择了HTTPS。
概念
-
TCP(摘抄自:https://baijiahao.baidu.com/s?id=1638084913643758145&wfr=spider&for=pc)
TCP是面向连接的协议,为用户进程提供可靠的全双工字节流。通过这种方式,可以确保可靠有序的数据包,并且可以支持流量控制。-
OSI网络图层
-
各层数据组装
- TCP协议数据结构
3.1. Seq(上图中序号)表示接收方处理数据包的开始序号
3.2. Ack(上图中确认序号)表示即将处理接收方数据包的开始序号
Seq与Ack的理解:打个比方就是两个人协作工作,每个人都说你帮我从A开始弄,我给你从B开始搞。然后每隔一段时间再次沟通怎么搞。 -
TCP协议生命周期
-
-
SSL(摘抄自:https://cloud.tencent.com/developer/article/1425339)
SSL由Netscape公司于1994年创建,它旨在通过Web创建安全的Internet通信。它是一种标准协议,用于加密浏览器和服务器之间的通信。它允许通过Internet安全轻松地传输账号密码、银行卡、手机号等私密信息。- SSL证书:由受信任的CA机构颁发的遵守SSL协议的数字证书
-
SSL证书使用
客户端在接受到服务端发来的SSL证书时,会对证书的真伪进行校验,以浏览器为例说明如下:
(1)首先浏览器读取证书中的证书所有者、有效期等信息进行一一校验;
(2)浏览器开始查找操作系统中已内置的受信任的证书发布机构CA,与服务器发来的证书中的颁发者CA比对,用于校验证书是否为合法机构颁发;
(3)如果找不到,浏览器就会报错,说明服务器发来的证书是不可信任的;
(4)如果找到,那么浏览器就会从操作系统中取出 颁发者CA 的公钥,然后对服务器发来的证书里面的签名进行解密;
(5)浏览器使用相同的hash算法计算出服务器发来的证书的hash值,将这个计算的hash值与证书中签名做对比;
(6)对比结果一致,则证明服务器发来的证书合法,没有被冒充;
(7)此时浏览器就可以读取证书中的公钥,用于后续加密了;
-
HTTPS(摘抄自:https://cloud.tencent.com/developer/article/1425339)
HTTPS是HTTP的安全版本,它可以通过SSL / TLS连接保护在线传输的任何通信。简而言之,HTTPS=HTTP+SSL。如果想要建立HTTPS连接,则首先必须从受信任的证书颁发机构(CA)注册 SSL证书。安装SSL证书后,网站地址栏HTTP后面就会多一个“S”,还有绿色安全锁标志。-
HTTPS的交互图
(1)看蓝色的部分是tcp链接。所以https的加密层也是在tcp之上的。
(2)客户端首先发起clientHello消息。包含一个客户端随机生成的random1 数字,客户端支持的加密算法,以及SSL信息。
(3)服务器收到客户端的clientHello消息以后,取出客户端法发来的random1数字,并且取出客户端发来的支持的加密算法,
然后选出一个加密算法,并生成一个随机数random2,发送给客户端serverhello让客户端对服务器进行身份校验,服务端通过将自己的公钥通过数字证书的方式发送给客户端。
(4)客户端收到服务端传来的证书后,先从 CA 验证该证书的合法性,验证通过后取出证书中的服务端公钥,再生成一个随机数 Random3,再用服务端公钥非对称加密 Random3 生成 PreMaster Key。并将PreMaster Key发送到服务端。
(5)服务端通过私钥将PreMaster Key解密获取到Random3,此时客户端和服务器都持有三个随机数Random1 Random2 Random3,双方在通过这三个随即书生成一个对称加密的密钥.双方根据这三个随即数经过相同的算法生成一个密钥,而以后应用层传输的数据都使用这套密钥进行加密。Change Cipher Spec Finished:告诉客户端以后的通讯都使用这一套密钥来进行。
(6)最后ApplicationData 全部使用对称加密的原因就是非对称加密太卡,对称加密不影响性能。所以实际上也看的出来,HTTPS的真正目的就是保证对称加密的 密钥不被破解,不被替换,不被中间人攻击,如果发生了上述情况,那么HTTPS的加密层也能获知,避免发生事故。
-
部署HTTPS服务端
- SSL证书颁发机构
Let’s Encrypt 是一家免费、开放、自动化的证书颁发机构(CA),为公众的利益而运行。它是一项由 Internet Security Research Group(ISRG)提供的服务。 - 申请SSL证书(摘抄自:https://my.oschina.net/u/2306127/blog/1929904)
# 以ubuntu18.04为例(用的阿里云,有的依赖已有就不安装了) # 安装Git 和bc ,并从github上将代码克隆到本地 sudo apt install git bc sudo git clone https://github.com/certbot/certbot /opt/certbot-master # 停掉nginx(因为需要用到80端口连接验证) sudo service nginx stop # 安装过程中会设置邮箱地址,邮箱地址是用来接收紧急通知和找回密钥的 sudo /opt/certbot-master/letsencrypt-auto certonly --standalone -d 域名 # 启用更安全的加密方式 # 默认是 SHA-1 形式,而现在主流的方案应该都避免 SHA-1,为了确保更强的安全性,我们可以采取迪菲-赫尔曼密钥交换。 openssl dhparam -out /etc/ssl/certs/dhparam.pem 2048
- 配置SSL证书(同上)
# 在Nginx的域名配置文件中添加 server { #nginx 监听端口,443为默认https端口,ssl指使用https,如果多处监听了80则将 default删除 listen 80 default backlog=2048; listen 443 ssl; # 服务器名称 server_name bnxb.com; # https证书公钥 ssl_certificate /etc/letsencrypt/live/bnxb.com/fullchain.pem; # https证书私钥 要注意保存! ssl_certificate_key /etc/letsencrypt/live/域名/privkey.pem; # 支持的加密协议 ssl_protocols TLSv1 TLSv1.1 TLSv1.2; ssl_prefer_server_ciphers on; #nginx默认会使用Diffiel-Hellman交换密钥是1024位的,相对不安全,所以需要替换使用更安全的 ssl_dhparam /etc/ssl/certs/dhparam.pem; # 支持的加密套件 ssl_ciphers 'ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:kEDH+AESGCM:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:DHE-RSA-AES256-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:AES:CAMELLIA:DES-CBC3-SHA:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!aECDH:!EDH-DSS-DES-CBC3-SHA:!EDH-RSA-DES-CBC3-SHA:!KRB5-DES-CBC3-SHA'; # 定义session过期时间 ssl_session_timeout 1d; ssl_session_cache shared:SSL:50m; ssl_stapling on; ssl_stapling_verify on; # 如果是全站 HTTPS 并且不考虑 HTTP 的话可以为响应头添加要求浏览器使用https重定向的 header add_header Strict-Transport-Security max-age=15768000; # 禁止外部站点iframe add_header X-Frame-Options DENY; # 以下就是自身服务的配置 ... }
- 续期SSL证书(同上)
# 手动续期 sudo ./opt/certbot-master/letsencrypt-auto renew --force-renewal # 自动续期 touch renewCerts.sh sudo chmod +x renewCerts.sh # 将以下脚本内容添加到renewCerts.sh中 #!/bin/sh # This script renews all the Let's Encrypt certificates with a validity < 30 days if ! /opt/certbot-master/letsencrypt-auto renew > /var/log/letsencrypt/renew.log 2>&1 ; then echo Automated renewal failed: cat /var/log/letsencrypt/renew.log exit 1 fi nginx -t && nginx -s reload # 配置系统定时执行脚本 crontab -e # 每个月一号凌晨执行 # m h dom mon dow command 0 0 1 * * sh /root/renewCerts.sh >/dev/null 2>&1 &
简单HTTPS客户端
强烈建议利用Wireshark查看TCP生命周期中的网络包信息,利用Fiddler拦截HTTP与HTTPS请求并篡改响应数据玩玩。以下是Java版本的简化版https客户端,可以进一步了解https的代码实现。
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import javax.net.ssl.X509TrustManager;
import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.net.URL;
import java.net.URLConnection;
import java.security.GeneralSecurityException;
import java.security.cert.X509Certificate;
public class HttpsTest {
public static void main(String[] args) {
// 初始化SSL上下文
initSSLContext();
try {
// 设置请求的url
URL url = new URL("https://test.api.xxx.com/manager/login");
URLConnection urlConn = url.openConnection();
urlConn.setRequestProperty("Content-Type", "application/json;charset=UTF-8");
// 设置请求的body内容
urlConn.setDoOutput(true);
OutputStreamWriter writer = new OutputStreamWriter(urlConn.getOutputStream());
writer.write("{userName: \"admin\", password: \"666\"}");
writer.flush();
writer.close();
// 执行Https请求并获取响应数据的输入流
InputStream is = urlConn.getInputStream();
// 读取响应信息
BufferedReader in = new BufferedReader(new InputStreamReader(is));
String line;
while ((line = in.readLine()) != null) {
System.out.println("响应数据:" + line);
}
} catch (Exception e) {
e.printStackTrace();
}
}
// 通过HttpsURLConnection提供的静态方法注册默认的SSLSocketFactory与HostnameVerifier
public static void initSSLContext() {
// 初始化TLS协议SSLContext
SSLContext sslContext;
try {
sslContext = SSLContext.getInstance("TLS");
X509TrustManager[] xtmArray = new X509TrustManager[]{xtm};
sslContext.init(null, xtmArray, new java.security.SecureRandom());
} catch (GeneralSecurityException gse) {
throw new RuntimeException("初始化SSL上下文失败!", gse);
}
//为javax.net.ssl.HttpsURLConnection设置默认的SocketFactory和HostnameVerifier
HttpsURLConnection.setDefaultSSLSocketFactory(sslContext.getSocketFactory());
HttpsURLConnection.setDefaultHostnameVerifier(hnv);
}
// 利用内部类定义信任所有服务器证书的证书管理器
private static final X509TrustManager xtm = new X509TrustManager() {
public void checkClientTrusted(X509Certificate[] chain, String authType) {}
public void checkServerTrusted(X509Certificate[] chain, String authType) {
System.out.println("证书信息: " + chain[0].toString() + ", 认证方式: " + authType);
}
public X509Certificate[] getAcceptedIssuers() {
return null;
}
};
// 利用内部类定义主机校验名校验器
private static final HostnameVerifier hnv = (hostname, session) -> {
System.out.println("主机信息:" + hostname);
return true;
};
}