- Socket 编程发展
- OpenResty 简介
- Lua 入门
- Nginx
- 子查询
- 不同阶段共享变量
- 防止 SQL 注入
- 如何发起新 HTTP 请求
- 访问有授权验证的 Redis
- select+set_keepalive 组合操作引起的数据读写错误
- redis 接口的二次封装(简化建连、拆连等细节)
- redis 接口的二次封装(发布订阅)
- pipeline 压缩请求数量
- script 压缩复杂请求
- 动态生成的 lua-resty-redis 模块方法
- LuaCjsonLibrary
- json解析的异常捕获
- 稀疏数组
- 空table编码为array还是object
- PostgresNginxModule
- 调用方式简介
- 不支持事务
- 超时
- 健康监测
- SQL注入
- LuaNginxModule
- 执行阶段概念
- 正确的记录日志
- 热装载代码
- 阻塞操作
- 缓存
- sleep
- 定时任务
- 禁止某些终端访问
- 请求返回后继续执行
- 调试
- 请求中断后的处理
- 我的 lua 代码需要调优么
- 变量的共享范围
- 动态限速
- shared.dict 非队列性质
- 正确使用长链接
- 如何引用第三方 resty 库
- 典型应用场景
- 怎样理解 cosocket
- 如何安全启动唯一实例的 timer
- 如何正确的解析域名
- LuaRestyDNSLibrary
- 使用动态 DNS 来完成 HTTP 请求
- LuaRestyLock
- 缓存失效风暴
- HTTPS 时代
- 动态加载证书和 OCSP stapling
- TLS session resumption
- 测试
- Web 服务
- 火焰图
- 如何定位问题
- module 是邪恶的
- FFI
- 什么是 JIT
TLS session resumption
在上一节我们介绍了 OCSP stapling。本节我们介绍另一种 HTTPS 性能优化的技巧,TLS session resumption。
一个完整的 TLS 握手需要两次:
- Client 发送 ClientHello;Server 回复 ServerHello
- Client 回复最终确定的 Key,Finished;Server 回复 Finished
- 握手完毕,Client 发送加密后的 HTTP 请求;Server 回复加密后的 HTTP 响应
这一过程的花费是 2RTT(Round-Trip-Time)。意味着仅仅部署了 HTTPS,就会让你的 Web 应用的响应都慢上 2RTT。
花在信息传递过程中的延迟在整个响应时间的占比不容小觑,看看国内有多少 CDN 厂商就知道了。
为什么强调是“完整的”呢?因为通过 TLS session resumption,我们可以复用未过期的会话,把 RTT 减低到 1,甚至更低。
Session ID
Session ID 是最早的 TLS session resumption 方案。除了某些上古浏览器,大部分客户端都支持它。
Client 发送的 ClientHello 当中,就包含了一个 Session ID。服务器接收到 Session ID 之后,会返回之前存储的 SSL 会话。这么一来,重建连接就只需一次 TLS 握手。
- Client 发送 ClientHello(包含 Session ID);Server 回复 ServerHello 和 Finished
- 握手完毕,Client 发送加密后的 HTTP 请求;Server 回复加密后的 HTTP 响应
Nginx 自身支持 Session ID,但有个问题,它的会话最多只能存储在共享内存里面。服务器和客户端上次握手建立的会话,只有某个服务器自己认得,换个服务器就忘光光了。当然你也可以要求负载均衡的时候只用 IP hash,尽管在实际情况中这么做不太现实。
OpenResty 提供了 ssl_session_fetch_by_lua*
和 ssl_session_store_by_lua*
这两个支持协程的阶段,以及跟 Session id 相关的 ngx.ssl.session
模块,把存储的决定权交给开发者手中。
你可以把 session 放到独立的 Redis 或 Memcached 服务器上。
Session Tickets
不过你可能已经不需要额外折腾 ssl_session_*
的代码。因为 Session ID 已经过时了。
TLSv1.1 提供了名为 Session Tickets 的拓展,用来代替之前的 Session ID 方案。
Session ID 方案要求服务端记住会话状态,有违于 HTTP 服务无状态的特点。Session Tickets 方案旨在解决这个问题。
Session Tickets 跟 Session ID 差不多,只是有点关键上的不同:现在轮到由客户端记住会话状态。
- Client 发送 ClientHello(包含 Session Ticket);Server 回复 ServerHello 和 Finished
- 握手完毕,Client 发送加密后的 HTTP 请求;Server 回复加密后的 HTTP 响应
服务端仅需记住当初用于加密返回给客户端的 Ticket 的密钥,以解密客户端握手时发送的 Session Ticket。
这么一来,只要在不同的服务器间共享同一个密钥,就能避免会话丢失的问题,不再需要独立的 Redis 或 Memcached 服务器来存储会话信息。
在高兴之余看下两个坏消息:
- Session Tickets 不具有前向安全性,所以你需要定期轮换服务端用于加密的密钥。
- 只有现代浏览器才支持这一 TLS 拓展。比如 Win7 下的 IE 就不支持。
对于 Nginx,你需要关注两个指令:ssl_session_tickets
和 ssl_session_ticket_file
。
前者启用 Session Tickers 支持,后者决定具体用到的密钥。如果不配置后者,则使用随机生成的密钥。这意味着每台服务器返回的 session ticket 会不一样。
如果你需要管理一整套 OpenResty 集群,可以看下 lua-ssl-nginx-module 这个模块,它可以实现集群层面上的密钥轮换(且无需重启 Worker 进程)。
尽管 lua-ssl-nginx-module 只提供了跟 memcache 配套使用的接口,但是参照 lualib/ngx/ssl/session/ticket/key_rotation.lua
,实现自己的一套密钥存储/同步方案不过是照葫芦画瓢。
关键在于 lualib/ngx/ssl/session/ticket.lua
其中的这两个函数:
update_ticket_encryption_key(key, nkeys)
:插入新的密钥,之前的密钥会被轮转成解密密钥,最多保留nkeys
个密钥。update_last_ticket_decryption_key(key)
:替换当前的解密密钥。
0 RTT!?
既然通过 Session ID/Tickets,我们已经把 RTT 减到了 1,能不能更进一步,减到 0?
初看像是天方夜谭,但最新的 TLSv1.3 确实允许做到这一点。
在继续之前先看下 TLSv1.3 的支持情况:Nginx 需要 1.13+ 的版本,外加 OpenSSL 1.1.1。客户端方面,截止到写作本文的时间,Firefox 和 Chrome 的 nightly build 版本均支持。
0 RTT 是 TLSv1.3 的可选功能。客户端和服务器第一次建立会话时,会生成一个 PSK(pre-shared key)。服务器会用 ticket key 去加密 PSK,作为 Session Ticket 返回。
客户端再次和服务器建立会话时,会先用 PSK 去加密 HTTP 请求,然后把加密后的内容发给服务器。服务器解密 PSK,然后再用 PSK 去解密 HTTP 请求,并加密 HTTP 响应。
- Client 发送 ClientHello(包含 PSK)和加密后的 HTTP 请求;Server 回复 ServerHello 和 Finished 和加密后的 HTTP 响应。
这就完事了。
由于 HTTPS 握手已经跟 HTTP 请求合并到一起,确实是当之无愧的 0 RTT 呢。
在高兴之余看下两个坏消息:
- PSK 不具有前向安全性,所以你依然需要定期轮换服务端用于加密的 ticket key。
- 0 RTT 不提供 non-replayable 的保障,所以需要更上层的 HTTP 协议提供防重放的保障。比如只在幂等的 HTTP 方法中启用 0 RTT,或者实现额外的时序标记。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论