3、HTTPS与TLS:TLS握手协议详解、证书链验证、在Nginx中配置HTTPS

说实话,HTTPS 现在已经是全栈开发的标配了。我见过不少团队,开发环境用 HTTP,上线前才匆匆忙忙配证书。结果呢?各种坑。今天咱们就把 TLS 握手、证书链、Nginx 配置这三个核心点彻底讲透。

3.1 TLS 握手协议:到底在握什么?

很多人以为 TLS 握手就是「客户端发个请求,服务端返回证书」。其实没那么简单。我当年第一次抓包看 TLS 握手,看到十几个来回的报文,直接懵了。

说白了,TLS 握手要干三件事:

  • 身份验证:确认服务端(有时也包括客户端)是它声称的那个人
  • 密钥协商:双方安全地商量出一个对称加密密钥
  • 参数确认:确定用什么加密算法、什么协议版本

嗯,这里要注意,TLS 1.3 比 1.2 少了一个来回,但核心逻辑没变。咱们以最常见的 TLS 1.2 完整握手为例:

ClientHello  →  客户端发送支持的TLS版本、加密套件列表、随机数
                ←  ServerHello  服务端选定版本和加密套件、发送随机数
                ←  Certificate  服务端发送证书链
                ←  ServerHelloDone  服务端说「我发完了」
ClientKeyExchange  →  客户端生成预主密钥,用服务端公钥加密后发送
ChangeCipherSpec  →  客户端说「我要开始加密通信了」
Finished  →  客户端发送加密后的握手摘要
                ←  ChangeCipherSpec  服务端也切换加密
                ←  Finished  服务端发送加密后的握手摘要

这里有个关键点:预主密钥。客户端生成的这个随机数,用服务端的公钥加密传输。只有持有私钥的服务端才能解密。我在项目中遇到过有人问:「那中间人拿到加密的预主密钥怎么办?」——他拿不到私钥,解不开。

核心结论:TLS 握手的本质,就是利用非对称加密安全地传递一个对称密钥。后续所有数据都用这个对称密钥加密,效率高得多。

3.2 证书链验证:信任是怎么传递的?

你想想看,浏览器凭什么相信服务端发来的证书?

答案就是证书链。我刚开始做安全时,觉得证书链就是个形式。直到有一次线上服务突然报证书错误,排查半天发现是中间证书没配全。从那以后,我对证书链再也不敢马虎。

证书链的结构是这样的:

根证书(CA根) → 中间证书 → 服务器证书(叶子证书)

验证过程分三步:

  1. 检查服务器证书:域名是否匹配、是否在有效期内、是否被吊销
  2. 验证签名链:用中间证书的公钥验证服务器证书的签名,再用根证书的公钥验证中间证书的签名
  3. 信任根证书:根证书必须存在于操作系统的受信任根证书列表中

这里有个常见的坑:证书链不完整。有些运维只配置了服务器证书,没配中间证书。某些浏览器会自动补全,但移动端或老旧系统就会报错。我曾经因为这个被客户投诉过——安卓 4.4 的设备死活连不上我们的 API。

我的建议:配置 HTTPS 后,用 openssl s_client -connect yourdomain.com:443 -showcerts 命令检查证书链是否完整。如果只返回一个证书,说明中间证书没配。

3.3 在 Nginx 中配置 HTTPS

好,理论讲完了,咱们来点实际的。Nginx 配置 HTTPS 其实不复杂,但细节决定成败。

先看一个最基础的配置:

server {
    listen 443 ssl http2;
    server_name example.com;

    ssl_certificate     /etc/nginx/ssl/example.com.pem;
    ssl_certificate_key /etc/nginx/ssl/example.com.key;

    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers HIGH:!aNULL:!MD5;

    location / {
        proxy_pass http://backend;
    }
}

嗯,这里要注意几个点:

  • 证书文件.pem 文件要包含完整的证书链。我习惯把服务器证书和中间证书拼在一起,服务器证书在上,中间证书在下
  • 私钥文件:权限一定要设置为 600,只有 root 能读。我见过有人把私钥设成 644,结果被扫描工具扫出来了
  • 协议版本:建议只开 TLSv1.2 和 TLSv1.3。SSLv3 和 TLSv1.0/1.1 都有已知漏洞,别用了

再来看一个更安全的配置:

server {
    listen 443 ssl http2;
    server_name example.com;

    ssl_certificate     /etc/nginx/ssl/fullchain.pem;
    ssl_certificate_key /etc/nginx/ssl/privkey.pem;

    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256;
    ssl_prefer_server_ciphers on;

    # 开启 OCSP Stapling
    ssl_stapling on;
    ssl_stapling_verify on;
    resolver 8.8.8.8 8.8.4.4 valid=300s;
    resolver_timeout 5s;

    # 安全头
    add_header Strict-Transport-Security "max-age=63072000" always;
    add_header X-Content-Type-Options nosniff;
    add_header X-Frame-Options DENY;
}

避坑指南:我曾经在配置 OCSP Stapling 时,忘了配 resolver 指令。结果 Nginx 无法解析 OCSP 响应器的域名,stapling 一直不生效。客户端每次连接都要自己去验证证书吊销状态,性能大打折扣。

配置完成后,记得用 nginx -t 测试配置是否正确。然后重启 Nginx:

nginx -t
systemctl reload nginx

最后,用 curl -vI https://example.com 验证一下。如果看到 SSL connection using TLSv1.3 之类的输出,说明配置成功了。

3.4 性能优化小贴士

HTTPS 确实比 HTTP 慢一点,但差距已经很小了。我做过压测,TLS 1.3 的握手延迟比 TLS 1.2 少了将近一个 RTT。如果你还在用 TLS 1.2,建议尽快升级。

另外,开启 http2 也能提升性能。HTTP/2 的多路复用特性,配合 HTTPS 使用效果更好。配置里加个 http2 就行,Nginx 会自动处理。

嗯,今天就先聊到这。下一章咱们会讲如何自动化管理证书,包括 Let's Encrypt 的自动续期。到时候再细聊。