TLS 会话票据能加速重连,但这张由服务器签发、你原样回传的票据,本身就是可关联的 ID——不需要 Cookie、JavaScript,也不碰任何存储 API。
大多数关于追踪的讲解只停留在浏览器这一层:Cookie、localStorage、用 JavaScript 算出来的指纹。但在这一切之下,HTTPS 还有一层,而且它自己保存状态。TLS 握手完成后,服务器可以交给浏览器一小块不透明的数据——会话票据(session ticket)。浏览器把它存起来,下次连接时原样回传,从而省掉大部分握手。它的本意是让网页更快;但它恰好也是一个标识符:由服务器签发,由你原样出示,而任何"清除 Cookie"按钮都碰不到它。
核心要点
- 会话恢复是正当的性能特性。 在 TLS 1.3 中,服务器在握手后发送
NewSessionTicket消息;客户端在之后的连接里凭它用预共享密钥恢复会话,不必重走完整握手。 - 票据天生就是一个 ID。 票据由服务器生成,客户端存着却读不懂,回传时一字不改。只要服务器在票据里编码、或在后台记下某个标识符,就能认出回访的客户端——全程不涉及 JavaScript、Cookie 或存储 API。
- 有效期就是追踪窗口,而且可以接力续期。 RFC 8446 把单张票据声明的有效期上限定为七天,但服务器可以在每次恢复时再签发一张新票据。只要客户端在窗口内一直回访,这条关联就能一次次延长。
- 票据在网络上是明文可见的。 它放在 ClientHello 里,因此链路上的被动观察者能把复用同一张票据的连接关联起来。RFC 8446 正是出于这个原因,要求客户端不要在多个连接中复用同一张票据。
- 实际上它不是全局超级 Cookie。 现代浏览器会限定会话缓存的作用范围,让第三方无法跨无关站点恢复会话,并在隐私浏览的边界丢弃缓存——但具体行为因浏览器和版本而异,请自行验证,别想当然。
会话恢复是干什么用的
完整的 TLS 握手是有成本的:要一次往返来协商参数,要发送并验证证书,还要计算签名。一个页面开十几个连接,这些开销就累积起来了。会话恢复让已经互相认证过的客户端和服务器跳过最耗时的那部分。
在 RFC 8446 定义的 TLS 1.3 中,这靠预共享密钥(PSK)实现。握手完成后,服务器可以发送一条或多条 NewSessionTicket 消息(第 4.6.1 节),每条都带有一张不透明的票据、一个有效期,以及客户端派生对应 PSK 所需的材料。下次连接时,客户端在 ClientHello 的 pre_shared_key 扩展里出示这张票据(第 4.2.11 节),再用 binder 证明自己持有对应的密钥,服务器就可以接受恢复,不必再走完整的证书握手。
同一机制还支撑了 0-RTT 早期数据(第 2.3 节):客户端在第一轮发送(first flight)中就能带上应用数据。RFC 坦言这有代价,尤其是早期数据可能被重放,并在第 8 节专门展开。但就本文而言,结论很简单:会话恢复是个好特性,这里没有什么后门。隐私问题出在谁持有状态、状态能存活多久,而不是密码学本身有缺陷。
TLS 1.2 也有同样的思路,分两种形式——会话 ID 和会话票据(RFC 5077)——所以下文所说的并不是 TLS 1.3 才有的新问题。学术界多年前就指出过这种可关联性,常被引用的是 Sy、Burkert、Federrath 和 Fischer 发表在 ACSAC 2018 的论文 Tracking Users across the Web via TLS Session Resumption。
票据如何变成标识符
票据对客户端是不透明的。浏览器看不到里面是什么,只是把服务器给的东西存下来。正是这种不透明,让它成为服务器手里好用的积木,也让它成了潜在的追踪手段。
常见的设计有两种:
- 自加密票据。 服务器把会话状态打包进票据,用只有自己掌握的密钥加密,本地什么也不存。任何持有该密钥的服务器都能解开它。
- 服务器端查表。 票据只是一个随机句柄,服务器把状态存在以这个句柄为键的表里。
无论哪种,票据里装什么都由服务器说了算,你回传的每个字节它也都看得到。协议并没有禁止服务器往里面塞一个每位客户端独有的标识符,或者干脆记下回来的是哪个票据值。TLS 1.3 确实会对票据的年龄做混淆(obfuscated_ticket_age 字段),但票据本身是原样发送的。结果就是一个稳定、可被服务器识别的 ID,藏在 TLS 握手里,而不在 HTTP 头或脚本可读的存储中。它和 ETag 缓存超级 Cookie 是同一种套路,只是低了一层——而且和 ETag 不同,页面里的 JavaScript 读不到、写不了,也删不掉它。
接力续期问题
七天上限听上去像是天然的约束。RFC 8446 确实规定服务器使用的票据有效期不得超过 604800 秒,也就是七天。但这个上限管的是单张票据,而不是票据背后的身份。
每次恢复成功后,服务器都可以再发一条新的 NewSessionTicket。客户端用新票据替换旧票据,下次访问出示的就是新的那张。链条上的每一环都会把窗口往后推,所以只要客户端每隔几天至少重连一次,就可以被无限期地跟下去,每张票据都把同一个底层身份往前传。声明的有效期能约束单张票据,却约束不了一个不停重新签发的服务器。
这种接力正是多数讲解略过的部分,也说明了为什么"票据一周后就过期"会低估你每天都访问的站点所带来的暴露。不过在实践中,浏览器往往会对缓存会话的复用时长设定更短的上限,而且内存中的缓存在浏览器彻底退出时就会消失——所以真正的上限通常由客户端决定,而不是 RFC 里的七天。
为什么实际上它不是超级 Cookie
如果据此认为"你访问过的任何网站都能用 TLS 票据跟踪你",那就错了。风险的边界取决于浏览器如何划定会话缓存的范围:
- 谁能恢复哪个会话。 你在一个站点上建立的会话,不应被嵌入在另一个无关站点上的同一第三方服务器恢复。按顶级站点对网络状态(连接、HTTP 缓存、TLS 会话)做分区是通行做法,这也和基于缓存的追踪广为人知之后浏览器对 HTTP 缓存的处理如出一辙。
- 隐私浏览与清除。 隐私窗口应当以空的会话缓存开始、在关闭时丢弃;完整的"清除所有数据"或重启浏览器,通常也会清空内存中的缓存。
请把以上内容看作对一类防御手段的描述,而不是对某个具体版本的承诺:各浏览器如何分区或清除会话缓存一直在变,因此我们刻意不在这里写版本号。仍然存在的是同站追踪:单个站点(或为它提供服务的 CDN)依然能认出你回访的连接,网络观察者也依然能关联那些明显复用了同一张票据的连接。
RFC 8446 中专门讨论客户端追踪防护的附录(附录 C.4)讲明了设计意图:客户端不应在多个连接中复用票据,因为复用会让被动观察者把这些连接关联起来。遵循这条建议的浏览器能限制链路上观察者获得的信息,但对签发票据的服务器本身无能为力。
另一个影响:会话恢复会改变指纹
还有一个副作用,和本站对网络层的系列内容直接相关。恢复会话时的握手,在网络上看起来和完整握手不一样。它会额外带上 pre_shared_key 扩展(必须是 ClientHello 中的最后一个扩展),里面装着票据和 binder,还可能再加上 early_data 扩展。因此,扩展列表和 ClientHello 的总长度都会与首次连接不同。
JA3、JA4 这类 TLS 指纹是根据 ClientHello 计算的,所以同一个浏览器在恢复的连接上,可能算出与新建连接不同的值。如果你在用 TLS 指纹做检测,或者正想弄明白为什么同一个客户端会出现两个哈希,会话恢复就是必须考虑的一个波动来源。具体原理见 TLS 指纹识别详解和 JA4+ 套件;至于更新的传输协议,HTTP/3 与 QUIC 指纹识别里也有同样的现象。
它在各类持久标识符中的位置
| 层 | 标识符 | 由谁存储 | 清除 Cookie 能清掉吗? |
|---|---|---|---|
| 脚本 | 持久访客 ID | 页面 JavaScript,分散在多种存储中 | 往往只能清掉一部分 |
| HTTP 缓存 | ETag 超级 Cookie | 浏览器缓存 | 不能 |
| TLS | 会话票据 | TLS 协议栈的会话缓存 | 不一定 |
要说明的不是每一层都是灾难——每一层都有浏览器的缓解措施——而是"我清过 Cookie 了"只覆盖了这一整摞状态存放点中最上面的一行。
你实际能做什么
这些几乎都无法在页面内部观察到,这本身就是问题所在。实际可行的做法是:
- 别指望清除 Cookie 就能重置 TLS 会话缓存。 "清除 Cookie"是否会顺带丢弃缓存的 TLS 会话,取决于浏览器;彻底退出并重启浏览器,或者执行"清除所有数据",是更可靠的办法。
- 隐私窗口是最干净的边界,因为它在设计上开始和结束时都不携带任何会话状态。
- 别指望有工具能把你的票据显示出来。 BrowserInsight 不会检查你的 TLS 票据,也不报告会话恢复状态。它能展示的是服务器从你的 TLS 握手中观察到的信息——协议版本、密码套件、ClientHello 长度和扩展哈希——就在指纹检测的网络卡片里,同样的信号也用于机器人检测工具。需要注意,这些数值描述的是承载这次请求的那条连接,而它本身也可能是一条恢复的连接。


