2. TLS 握手过程详解
TLS 握手,说白了就是客户端和服务器在正式传输数据之前,先互相打个照面、亮个身份、商量好怎么加密的一个过程。这个过程直接影响连接建立的速度,也就是用户能感受到的「卡顿时间」。我刚开始调优的时候,总觉得握手不就是来回发几条消息嘛,能有多慢?后来在线上压测才发现,一次完整的握手可能消耗掉几十毫秒甚至上百毫秒——对于高并发服务来说,这可不是个小数目。
一个实用的直觉:如果你用浏览器打开一个 HTTPS 页面感觉「转圈」了很久,那多半是握手阶段在折腾。减少握手开销,就是让用户少等那几圈。
2.1 完整握手流程(TLS 1.2)
我们先看最经典的 TLS 1.2 完整握手。它需要 2 个 RTT(往返时间)才能完成密钥协商。我习惯把它拆成三个阶段来看:
- 第一阶段:Hello 交换——客户端发 ClientHello,携带支持的密码套件列表、随机数;服务器回复 ServerHello,选定密码套件,并带上自己的证书。
- 第二阶段:密钥交换与认证——服务器发 ServerKeyExchange(如果需要),然后发 CertificateRequest(可选);客户端验证证书,发 ClientKeyExchange,告知自己的密钥材料。
- 第三阶段:握手完成——双方发送 ChangeCipherSpec,接着发 Finished 消息,确认握手成功。
嗯,这里要特别留意 证书链验证。我在项目中遇到过一个问题:服务器配了一张中间证书,但客户端没有缓存它,结果每次握手都要额外下载,握手时间直接翻倍。那个排查过程真是……一言难尽。
| 阶段 | 消息方向 | 关键内容 | 耗时因素 |
|---|---|---|---|
| Hello 交换 | C → S, S → C | ClientHello, ServerHello, 证书 | 证书大小、网络延迟 |
| 密钥交换 | C → S | ClientKeyExchange, 证书验证 | 非对称加密计算 |
| 握手完成 | C → S, S → C | ChangeCipherSpec, Finished | 对称密钥生成 |
避坑指南:我曾经在调一个金融接口时发现,客户端每次请求都重新验证整条证书链,包括根证书的 CRL(证书吊销列表)查询,导致握手耗时 200ms+。解决方案是开启 OCSP Stapling,把证书状态检查放到服务器端去做。改完之后,握手时间降到了 30ms 以内。这个坑我印象特别深。
2.2 会话恢复
完整握手的开销太大了,尤其是对短连接场景。TLS 提供了会话恢复机制,说白了就是复用之前协商好的会话信息,跳过一部分握手步骤。
会话恢复有两种实现方式:
- Session ID:服务器保存会话状态,客户端在第二次握手时直接带上 Session ID,如果服务器还记得,就可以跳过证书验证和密钥交换。只需要 1 个 RTT。
- Session Ticket:服务器把会话信息加密后发给客户端,客户端保存这个 Ticket,下次握手时带上,服务器解密后复用。我不太喜欢 Session ID,因为它依赖服务端存储,一旦服务器扩容或重启,Session ID 就失效了。个人更推荐 Session Ticket,无状态,弹性更好。
你想想看,一次完整的 2-RTT 握手,在会话恢复下变成了 1-RTT,这个优化效果立竿见影。我在压测一个消息推送服务时,开启 Session Ticket 后,QPS 提升了近 40%。
核心要点:会话恢复不是万能的。如果客户端和服务器之间长时间没有交互,Ticket 会过期(默认通常 24 小时)。另外,有些浏览器会把 Session Ticket 当作隐私数据,关闭后每次都是完整握手。这时候就需要其他方案了。
2.3 PSK 握手
PSK(预共享密钥)握手,说白了就是双方提前约定好一个密钥,不再走证书交换。这在 IoT 设备、企业内部服务之间很常见。PSK 握手流程比证书握手短得多:
- PSK 模式下:不需要证书,也不需要非对称密钥交换,直接基于预共享密钥生成会话密钥。握手可以做到 0-RTT 或 1-RTT。
- 身份标识:客户端通过 PSK Identity 告诉服务器自己用的是哪个密钥。
我曾经给一个车联网平台做优化,设备端和云端之间用的是 TLS PSK,握手时间稳定在 5ms 以内,比证书握手快了十多倍。当然,PSK 也有代价——密钥分发和管理比较麻烦。你没法像证书那样用 CA 统一签发,得自己搭建密钥管理系统。
注意:PSK 握手不验证身份来源,所以必须保证预共享密钥的安全存储。如果密钥被泄露,整个通信就暴露了。我在日志中看到过有人把 PSK 硬编码在客户端代码里——这种做法千万别学。
2.4 TLS 1.3 握手
TLS 1.3 的握手,可以说是对 TLS 1.2 的彻底重写。为什么?因为设计者觉得 2-RTT 太慢了,所以把握手压缩到了 1-RTT,甚至支持 0-RTT。我个人觉得这是 TLS 历史上最成功的一次升级。
TLS 1.3 握手的关键变化:
- 移除不安全的密码套件——只保留了 AEAD 密码(如 AES-GCM、ChaCha20-Poly1305),不再支持 RSA 密钥交换。这样做不光是为了安全,也是为了减少握手阶段的选择复杂度。
- 密钥协商提前到 Hello 阶段——客户端在 ClientHello 里就带上了自己的密钥共享(Key Share),服务器直接回复 ServerHello + 密钥共享 + 证书,双方各自计算出主密钥。一次往返就搞定了。
- 0-RTT(零往返)——如果客户端之前和服务器通信过,并且持有 PSK 或 Session Ticket,它可以在 ClientHello 之后立即发送应用数据。服务器如果接受,就直接使用,不需要额外握手。这个模式对于重连场景尤其好用。
为什么 TLS 1.3 能做到 0-RTT?说白了就是利用了之前协商好的密钥材料,加上时间戳和随机数做一次快速派生。但 0-RTT 有个安全问题:它容易遭受重放攻击。服务器收到了同样的 0-RTT 数据,可能会被重复处理一次。所以,0-RTT 通常只用于幂等操作,比如查询类请求。
# TLS 1.3 握手的简化流程(1-RTT)
Client Server
| -------- ClientHello (Key Share) --------> |
| <------ ServerHello (Key Share) --------- |
| <------ {EncryptedExtensions} ----------- |
| <------ {Certificate} ------------------ |
| <------ {CertificateVerify} ------------ |
| <------ {Finished} --------------------- |
| -------- {Finished} ---------------------> |
| ========= 应用数据 (保护模式) ===========> |
实测对比:我用同一个服务做过压测对比,TLS 1.2 完整握手平均耗时 45ms(证书 2KB),TLS 1.3 1-RTT 握手平均耗时 18ms,0-RTT 重连更是只有 2-3ms。对于 HTTP/2 多路复用场景,性能提升非常明显。
2.5 本章知识体系
我们把四种握手模式的核心关系梳理一下:
这张图把四种握手方式的时间和性能关系画出来了。你可以看到,从 TLS 1.2 完整握手的 45ms 到 TLS 1.3 0-RTT 的 2ms,优化空间超过 20 倍。关键就在于减少不必要的往返和计算。
我的建议:如果你的服务还在用 TLS 1.2,而且证书链比较长(超过 3 层),尽早规划升级到 TLS 1.3。不只是性能问题,安全上也有很大提升。我曾经帮一个电商客户做升级,只改了 Nginx 配置(ssl_protocols TLSv1.3; ssl_ciphers ...),首字节时间(TTFB)就降了 30%。这种「改配置就有效」的优化,可以说是性价比最高的。