TLS/SSL 协议详解
聊到 HTTPS 加密改造,TLS 协议是绕不开的核心。我做了这么多年运维,见过不少团队把证书一配就当完事,结果握手慢、兼容差、甚至中间人攻击都拦不住。说白了,不理解协议细节,改造就是碰运气。
这一节咱们把 TLS 的几个关键点掰开揉碎:1.2 和 1.3 差在哪、握手到底怎么握、密码套件怎么选、会话恢复怎么配。你想想看,如果连这些都不清楚,线上出了性能瓶颈你连排查方向都没有。
核心原则:能用 TLS 1.3 就别用 1.2;能用 ECDHE 就别用 RSA 密钥交换;会话恢复一定要开,尤其是移动端。
TLS 1.2 与 TLS 1.3 对比
我在一个金融项目里遇到过,客户强制要求 TLS 1.2,理由是“合规”。结果压测时发现握手延迟比 1.3 多了将近 80 毫秒。后来我跟他们解释:1.3 把握手从 2-RTT 降到了 1-RTT,甚至有 0-RTT 模式,对高并发和弱网环境简直是救命。
| 对比项 | TLS 1.2 | TLS 1.3 |
|---|---|---|
| 握手延迟 | 2-RTT(完整握手) | 1-RTT(完整握手) / 0-RTT(会话恢复) |
| 支持的密码套件 | 几十种,包含 CBC、RC4、静态 RSA | 仅 5 种 AEAD 套件,全部前向安全 |
| 密钥交换 | 支持 RSA、DH、ECDH(可静态) | 强制 ECDHE 或 DHE,不再支持静态密钥 |
| 压缩 / 重协商 | 支持(但均已废弃) | 完全移除 |
| 协议简化 | 扩展、选项多,配置复杂 | 精简握手消息,固定顺序 |
避坑:我曾经在升级 TLS 1.3 时忽略了一个细节——早期 nginx 的 OpenSSL 编译未开启 TLS 1.3 支持,导致配置下发后握手全失败。记得用 openssl ciphers -v -tls1_3 验证服务端实际支持的套件列表。
TLS 握手步骤详解
握手的本质是双方协商加密参数、交换密钥、验证身份。1.2 和 1.3 的流程差异很大,我画了一张图帮你建立整体认知。
从上图能直观看到:TLS 1.2 需要一次完整的往返(Client → Server → Client → Server)才能开始加密通信;而 1.3 把 ServerHello 之后的证书和完成消息合并,节约了一次 RTT。
一句话记:1.2 握手逛两次街才买完东西,1.3 一次搞定还能预售(0-RTT)。
握手细节拆解
咱们拿 TLS 1.2 的完整握手举例,我习惯把这些步骤顺口溜记成「HELLO → 证书 → 密钥 → 校验」:
- ClientHello:客户端发送支持的 TLS 版本、密码套件列表、随机数。
- ServerHello + Certificate + ServerHelloDone:服务端选定版本和套件,发证书和随机数。
- ClientKeyExchange:客户端用服务端证书里的公钥加密 Pre-Master Secret 发给服务端(如果是 RSA 密钥交换)。
- ChangeCipherSpec + Finished:双方各自计算会话密钥,切换加密模式,发送 Finished 消息验证完整性。
而 TLS 1.3 的握手更简洁:ClientHello 里直接带上客户端 DH 公钥(KeyShare),服务端回复时也带上自己的公钥和加密的扩展信息,双方一次性算出密钥。证书虽然仍然发送,但已经加密传输了。
注意:TLS 1.3 去掉了 ChangeCipherSpec 消息,部分中间件(如老式 F5 负载均衡)会把这当异常丢弃。我曾经因为这个排查了一整个下午……最后发现是硬件设备不兼容 1.3,只能回退。
密码套件选择
密码套件决定了加密算法、密钥交换、认证和完整性校验的全套方案。我最怕看到有人写 HIGH:!aNULL:!MD5 这种无脑配置——你想想看,古老算法都让握手变慢还留风险。
下面是生产环境的推荐套件表,我按优先级排列:
| 优先级 | 套件名称(TLS 1.3) | 套件名称(TLS 1.2 首选) | 说明 |
|---|---|---|---|
| 1 | TLS_AES_128_GCM_SHA256 | ECDHE-ECDSA-AES128-GCM-SHA256 | 速度快,兼容性好,推荐首选 |
| 2 | TLS_AES_256_GCM_SHA384 | ECDHE-RSA-AES128-GCM-SHA256 | 256 位密钥,合规要求时使用 |
| 3 | TLS_CHACHA20_POLY1305_SHA256 | ECDHE-ECDSA-CHACHA20-POLY1305 | 移动端和嵌入式设备更友好 |
| 4 | (无) | ECDHE-ECDSA-AES256-GCM-SHA384 | 仅 1.2 场景,性能略低于 AES-128 |
选择建议:我个人习惯在 nginx 里只配置 ECDHE+AESGCM:ECDHE+CHACHA20:!DSS:!aNULL:!eNULL,然后配合 ssl_protocols TLSv1.2 TLSv1.3。这样既保证前向安全,又删除了所有过时算法。
会话恢复
每次握手都走完整流程,在高并发场景下 CPU 负载高、延迟大。会话恢复就是“复用上一次的握手结果”。TLS 1.2 支持两种机制:Session ID 和 Session Ticket。
- Session ID:服务端保存会话状态,客户端提供 ID 即可恢复。缺点:服务端需要维护状态表,水平扩展时需共享 Session 存储(比如 Redis)。
- Session Ticket:服务端把会话状态加密成 Ticket 发给客户端,客户端下次带回来解密。无服务端状态,更适合分布式架构。
而在 TLS 1.3 里,会话恢复进化成 PSK(Pre-Shared Key) 模式。客户端可用之前协商好的 PSK 直接加密数据(0-RTT),但要注意重放攻击风险。
防重放:我曾经在支付网关中开启了 TLS 1.3 的 0-RTT,结果压测发现相同请求被服务器处理了两遍。解决方案是:在应用层加入一次性 nonce 或时间戳校验,服务端做去重。
配置方面,nginx 默认开了 Session Ticket,但 Ticket 密钥需要定期轮换。我习惯用 ssl_session_tickets off 并用外部 Redis 保存 Session Cache,配合 ssl_session_cache shared:SSL:10m 做内存缓存。这样既不用处理 Ticket 密钥分发,又能享受会话恢复的低延迟。
生产配置要点:
- Session Cache 大小:10MB 大约能存 40000 个会话,根据并发连接估算。
- Session Ticket 密钥轮换:至少每 24 小时换一次,可以用
openssl rand 48 > ticket.key生成。 - 0-RTT 启用前务必评估业务幂等性,非幂等操作不要用 0-RTT。