存储分区按嵌入站点与顶层页面对浏览器状态双重键控:同一个嵌入组件在两个网站上会拿到两个互不相通的独立存储桶,跨站共享的标识符就此失效。
在Web的大部分历史里,浏览器回答「这段代码该拿到哪份存储?」时只看一件事:代码来自哪个源(origin)。所以,一个嵌在一百个网站上的追踪组件,在这一百个网站上读写的都是同一份Cookie存储,而正是这份共享存储,让跨站追踪的成本低得惊人。存储分区(storage partitioning)换了一种问法:存储现在由两把键共同索引——拥有它的源,以及你实际正在访问的顶层站点——于是同一个嵌入组件出现在两个不同的网站上,拿到的是两个互不相干、彼此看不见的桶。
核心要点
- 核心思路就是双重键控。 浏览器不再只按资源的源来索引状态,而是按*(资源的源,顶层站点)*来索引。MDN的状态分区指南把Firefox的做法描述为「按被加载资源的源和顶层站点对所有客户端状态做双重键控」。
- 同一个嵌入组件出现在两个站点上,意味着两个桶,而不是一个。 嵌在
A.example和B.example里的追踪器,再也没法在一处存下ID、又在另一处读回来;它在A上看到的数据,和在B上看到的并不相同。 - 「Cookie」只是被覆盖范围的一部分。 分区还涵盖
localStorage、sessionStorage、IndexedDB、Cache API、Service Worker,以及HTTP缓存之类的网络状态——而且各家浏览器引擎分区的内容和方式并不完全一致。 - Storage Access API是刻意保留的例外通道。 嵌入的frame可以在用户手势之后调用
requestStorageAccess(),重新拿回自己正常的第一方Cookie,但这份授权只限定在某一对(顶层站点,嵌入站点)之间,而不是全局开启。 - 分区打断的是基于状态的跨站追踪,而不是追踪本身。 指纹不需要任何存储,也就无从分区——这正是这项改动反而让指纹对追踪者更有价值,而不是更没价值的原因。
「双重键控」到底是什么意思
浏览器传统上是按资源所加载位置的源(有时是可注册域名)来索引客户端状态的。MDN给出了经典例子:从https://example.com/hello.html加载的iframe,它能访问的Cookie、localStorage对象和缓存,都以example.com为键。无论浏览器是把example.com作为你正在浏览的页面(第一方)来加载,还是把它作为别人页面里的嵌入内容(第三方)来加载,这一点都成立。
追踪者利用的恰恰是这一点。在A.example和B.example上都放一个example.com的iframe或脚本,把用户标识符存进它的Cookie或localStorage,它就能在两个站点上读回同一个标识符。两个站点之间根本不需要任何串通;是共享的桶完成了关联。
分区加了第二把键。第一把键依然是被加载资源的源;第二把是顶层站点——多数情况下就是地址栏里那个页面的协议加可注册域名。当example.com被嵌进两个站点时,得到的结果是这样:
| 代码运行的位置 | 它拿到的存储桶 |
|---|---|
直接访问example.com | (example.com, example.com) |
example.com嵌入在A.example中 | (example.com, A.example) |
example.com嵌入在B.example中 | (example.com, B.example) |
于是原来的一个桶变成了三个互不相同的桶。追踪器依然有存储可用——只是没法把一个标识符从第一个桶带到另外两个桶里。它在被直接访问时存下的ID,再也没法在嵌入别处时取回。
注意哪些东西没有变:在example.com自己的站点上、在普通标签页里,一切照旧。分区只在代码运行于第三方上下文时才会起作用。
哪些东西会被分区(远不止Cookie)
最常见的误解,是把它当成「换了个名字的第三方Cookie拦截」。这是另一种思路。旧的策略是在第三方上下文里阻止访问某些存储API;分区则是给嵌入内容按每个顶层站点各提供一个独立的桶,所以无需阻止任何东西,隔离也能成立。
MDN关于Firefox的文档把受影响的范围分成三组:
| 分组 | 示例 | 在Firefox中的行为 |
|---|---|---|
| 可访问的存储 | localStorage、sessionStorage、DOM Cache、IndexedDB、Broadcast Channel、Shared Workers、Service Workers | 按顶层站点分区 |
| Cookie | 第三方Cookie | 默认分区,并提供重新获得非分区访问的途径(见下文) |
| 网络状态 | HTTP缓存、图片缓存、favicon缓存、连接池、DNS、HSTS、TLS会话标识符、OCSP、字体 | 永久分区;网站无法放宽 |
网络这一行对隐私格外重要,因为这些机制原本并不是用来存数据的,却可以被滥用为存储。这正是ETag与缓存超级Cookie背后的把戏:一个站点能设置、另一个站点又能读回的缓存响应,就是一条追踪通道。按顶层站点对HTTP缓存做分区,堵上的就是它的跨站版本。
各浏览器引擎的现状
老实说,并不存在一个统一的「Web平台现状」,因为各家引擎是各自在不同的时间点走到分区这一步的。下文只限于一手资料的说法;任何版本号都请以最新文档为准重新核对。
- **Safari(WebKit)**走在最前面。它在2017年推出的智能防追踪(Intelligent Tracking Prevention,ITP)会识别出能够跨站追踪用户的域名,并对它们的Cookie做分区,或者直接清除其网站数据。WebKit 2018年发布Storage Access API时的公告写道,一旦嵌入内容被归类为跨站追踪器,ITP就只给它「分区后的Cookie」。公告还提到,WebKit对该API的初始实现只涉及Cookie,并没有改变IndexedDB或
localStorage的分区方式。 - Firefox推出的是它称为状态分区(State Partitioning)的机制(对外宣传为Total Cookie Protection)。MDN记录了这次推进的过程:网络分区自Firefox 85起对所有用户默认开启,动态分区——也就是Cookie这一侧——自Firefox 103起默认开启,此前曾先后在严格模式(Firefox 86)和隐私浏览(Firefox 90)中作为可选项试行。
- Chromium走的是分步推进的路线。它没有一个总开关,而是分别对各部分做分区——先是HTTP缓存,之后是第三方存储API——并配合一个需要主动选择加入的分区Cookie机制CHIPS(见下文)。具体行为和版本门槛与Firefox不同,所以请查阅你所依赖的那个API的浏览器兼容性数据,不要想当然地认为各家一致。
对开发者而言,实际的后果是:一段依赖共享第三方状态才能在某个引擎里正常运行的代码,在另一个引擎里可能悄无声息地失效。对用户而言,实际的后果是:同一个追踪器,在你用不同浏览器时会面对不同的障碍。
分区Cookie:CHIPS与Partitioned属性
彻底封锁第三方Cookie会破坏正当的嵌入内容——一个按站点记住偏好的聊天组件或地图没有任何追踪动机,但一刀切的封锁会让它失灵。CHIPS(Cookies Having Independent Partitioned State)就是折中方案:站点用Partitioned属性把某个Cookie主动纳入分区,浏览器就会用双重键来存储它。
Set-Cookie: __Host-widget=abc123; Secure; Path=/; SameSite=None; Partitioned
以这种方式设置的Cookie,只有当嵌入内容加载于设置它时所在的同一个顶层站点之下,才会被发送回去。MDN的Storage Access API页面把这个取舍讲得很直白:一个无法跟着你跨站走的Cookie不存在隐私风险,所以浏览器会在请求中发送分区Cookie,并让嵌入资源可以使用它们——但同样因为这些Cookie不在站点之间共享,它们也不会自动在站点之间同步。
后半句正是这个模型的代价。像单点登录frame这类「登录一次、处处认得你」的嵌入内容,需要更强的手段。
例外通道:Storage Access API
Storage Access API允许跨站iframe申请拿回它正常的第一方Cookie。它于2018年在WebKit中率先引入——其公告交代了来龙去脉:开发者对ITP的反馈是,嵌入内容需要一种办法,来认证那些已经登录了其第一方服务的用户。
整个流程有两个方法:
document.hasStorageAccess()——返回一个promise,解析为该frame是否已经拥有非分区访问权限。document.requestStorageAccess()——申请访问权限;必须在用户手势期间调用(MDN称之为瞬时激活,transient activation),比如点击frame内的「登录」按钮。
这份授权意味着什么、不意味着什么:
- 它按配对授予,而不是全局。 MDN描述这项权限是以「顶层站点+嵌入站点」这一配对的结构存储的。授予给嵌在
embedder.com里的example.com的访问权限,不会延续到嵌在其他任何地方的example.com。 - 它恢复的是第一方Cookie,而不是嵌入方的数据。 WebKit的文章明确指出,存储访问「不会以任何方式放宽同源策略」——既不是第三方伸手去读宿主页面的存储,反过来也不是。
- 各浏览器的提示方式不同。 据MDN所述,Safari和Chrome会对此前没有获得过访问权限的嵌入内容弹出提示,而Firefox只有在某个源在超过一定数量的站点上申请过访问之后才会提示。在Chrome中,同一个相关网站集(related website set)内的嵌入方和被嵌入方可以跳过提示。
- Firefox还会通过启发式规则授予访问权限。 为了避免破坏常见的集成,它可以在用户与嵌入内容自己打开的弹窗交互之后,或者在一次快速的「跳转—交互—返回」序列之后,给该嵌入内容30天的访问权限。MDN把这些标注为过渡性措施,并提醒开发者不要依赖它们。
这套设计的目标是:重新获得访问权限,应当是在一次真实交互的语境下、针对具体站点做出的可见决定——而不是追踪者可以悄悄一键对所有站点同时打开的开关。
分区拦不住什么
把边界划清楚很有必要,因为「已分区」很容易被过度解读成「私密」。
- 第一方追踪不受影响。 站点依然可以用自己的Cookie认出自己的回头客。分区针对的只是跨站关联。
- 它不影响其他跨站手法。 用别的方式关联访问记录的技术——例如跳转追踪(bounce tracking),它让你短暂地经过追踪者自己的站点,从而使其以第一方身份运行——不在双重键的管辖之内。检测你登录了哪些站点也是如此。
- 它不是用户自主选择的隔离。 Firefox的容器是把你自己的各个身份彼此隔开;分区则是把每个站点的嵌入内容彼此隔开。Firefox两者同时生效,谁也替代不了谁——参见容器标签页与追踪。
- 它不是无痕模式。 隐私窗口在关闭时会丢弃状态;而分区在普通窗口里同样生效。至于网站为何仍会想方设法察觉隐私模式,参见无痕模式检测。
- 它不能防御持久标识符。 对于那些设计目标就是挺过清除操作的标识符——也就是持久访客ID这一话题——分区只是去掉了其中一条存储路径,并没有去掉目标本身。
为什么这把追踪者推向了指纹
这里的取舍不妨直说。分区打断的是基于状态的跨站追踪:追踪者在一个站点写进存储的标识符,再也没法在另一个站点读到。这一类技术全都依赖浏览器为每个源保留同一个共享桶,而这个前提已经不复存在。
指纹不走这条路。它是根据浏览器和硬件本来就会透露的信息计算出来的——Canvas与WebGL输出、字体、屏幕和硬件参数、音频行为——不会往你的设备上写入任何东西。没有涉及任何存储,浏览器就没有什么可分区、可清除、可拦截的。ID并没有被保存下来;它是在下次访问时,凭同样的输入被再次认出的。
这就是为什么堵上存储这条路,反而抬高了指纹这条路对所有仍想做跨站关联的人的价值。如果你想看看自己的浏览器暴露了哪些信号,我们的浏览器指纹识别指南讲解了其中的原理,指纹检测则可以给你一份实时读数。
常见问题
存储分区和拦截第三方Cookie是一回事吗?
不是。拦截是在第三方上下文里拒绝嵌入内容访问存储。分区则是给它存储——每个顶层站点一个独立的桶——所以嵌入内容照常工作,但它在一个站点上存下的东西,在另一个站点上看不到。
分区会破坏嵌入式登录和小组件吗?
有可能。原先依靠一个跨多站共享的Cookie来认出已登录用户的嵌入内容,会在每个站点上遇到一个全新的空桶。官方支持的修复方式是使用Storage Access API的requestStorageAccess()(需要用户手势),或者对于按站点区分的状态,使用带Partitioned属性的Cookie。
分区能阻止网站在它自己的页面上追踪我吗?
不能。第一方Cookie是以你当前所在的站点为键的,所以分区对它不做任何改动。它去掉的,是通过共享的嵌入存储在互不相关的站点之间关联你活动的能力。
所有浏览器的存储分区方式都一样吗?
不一样。Safari、Firefox和Chromium引入分区的时间和范围各不相同,覆盖哪些存储API、何时弹出提示等细节也有差异。在依赖任何一种行为之前,请先查阅该API最新的兼容性数据。
结语
存储分区是一个小改动,却带来了大影响:把顶层站点加为第二把键,意味着嵌入式追踪器在一个站点上的存储,就是和它在另一个站点上的存储完全不同的桶。这去掉了廉价跨站追踪所依赖的共享状态捷径,同时为确实需要它的正当场景留下了一条受控的回路——Storage Access API,或者主动选择加入的PartitionedCookie。它并没有消除追踪;它只是把较量转移到了完全不需要存储的地方,这也正是为什么,一旦你的浏览器对状态做了分区,理解指纹只会更重要,而不是更不重要。
推荐阅读:


