禁用第三方 Cookie 战记:我的第三方 Cookie 怎么办

本文适用人群:受到第三方 Cookie 影响,想要一些第三方 Cookie 解决方案的战友

本文可能会是我插图最多的文章之一,因为要解释清楚各种概念,可能需要引用很多张图片辅助消化。

尽管 Google 在 2022 / 2023 疯狂铺垫自己要禁用第三方 Cookie,大家马上改造,但实际上大部分人的工作还是在死线前后死亡冲刺。Google 官方宣称禁用第三方 Cookie 是为了「减少跨网站跟踪,同时确保让每个人都能免费访问在线内容和服务的功能」,但实际上好处还没感受到,却带来了一大堆麻烦。

Story Start

在开始之前,首先简单介绍下第三方的定义,Google 的解释是:

第三方是指由与您正在访问的网站不同的网域提供的资源。 例如,网站 foo.com 可能会使用来自 google-analytics.com 的分析代码(通过 JavaScript)、来自 use.typekit.net 的字体(通过链接元素)以及来自 vimeo.com 的视频(在 iframe 中)。另请参阅第一方。

也就是说,跨站携带的 Cookie 就是第三方 Cookie

那么下一个问题,跨站的定义是什么,或许你在面试题中会听到「跨域」、「跨站」,他们俩到底有什么区别:

  • 跨域:域名、端口号、协议有一个不一样,就是跨域的
  • 跨站:eTLD 相同的站点,不用管端口号,但需要注意协议
25486.png
1937.png

请注意:下文我们说的所谓的「同站」、「相同站点」都是基于这一定义衍生的名词。

我为什么要改造

实际上当你看到这篇文章才开始改造时已经晚了,因为 Chrome 已经开始进行 1% 的灰度了:

38045.png

也就是说,已经有部分用户会受到影响了,每个人都身处漩涡之中,不过好处是,在最差的情况下,你可以选择引导用户关闭第三方 Cookie。

关于这个的教程可以参考:support.google.com/accounts/an...,本文不多做赘述。

同时需要注意,Safari 和 Chrome 的解决方案是不同的,这点在下文介绍解决方案时会再次说明,而 Safari 其实已经禁用第三方 Cookie 很久了,所以如果你没有考虑过,现在也不用想得太复杂。

所以解决方案是什么

在明确解决方案之前,首先我们需要知道有那些场景,Google 为不同的场景提供了不同的解决方案:

  1. 用户标识符:各种数据统计上报可能会用 Cookie 来做用户标识符(uid / fingerprint),通过数据上报进行各种统计(比如 PV、UV)
  2. iframe:过于典型,嵌入站点常规使用 Cookie 就会遇到
  3. 个性化投放:用于进行广告归因和行为分析并进行投放
  4. SSO 登录:跨站共享登录身份
  5. CSRF Token:防止 CSRF 攻击

而针对这些场景,Google 提供了一系列解决方案,包括但不限于:

  • Topics API:用于解决用户个性化投放(场景 3)
  • Attribution Reporting:用于衡量广告点击或观看何时促成转化,例如在广告客户网站上完成购买。(场景 3)
    • Protected Audience API:设备端广告竞价,用于向再营销和自定义受众群体投放广告,无需进行跨网站第三方跟踪。(场景 3)
  • FedCM:支持联合身份服务,可让用户登录网站和服务。(场景 4)
  • 私密状态令牌:可通过跨网站交换有限的非身份信息来防范欺诈和反垃圾内容。(场景 5)

值得一提的是,根据我个人对于 FedCM 的理解,结合Federated Credential Management API 开发者指南,FedCM 像是身份提供方与浏览器的梦幻联动,因此更像是 Chrome Only 级别的解决方案。

而场景 1 用户标识符,参考 Google Analytics SDK 的实现,我们其实可以以前端的方式写入 Cookie,再用 querystring 拼接身份标识符来解决上报问题

而上述介绍的 API 目前还可能受到兼容性的挑战,因此本文更多希望站在第三方 Cookie 本身来讨论问题。

因此本文的重点在对于 iframe(场景 2) 和 SSO (场景 4) 的解决方案,并且尽可能的考虑到方案的通用性。

解决方案

狠一点的话走代理、加绑同站域名当然也是一种方式,但大部分场景下没有办法这么简单粗暴。

LocalStorage / Querystring

最简单的 Cookie 平替就是 localstorage 或者 querystring。

比如上面我们曾经说过的 Google Analytics SDK,假设在某些场景下(比如 iframe)前端无法顺利的写入 Cookie,也可以将值写入 localstorage,在上报时读取 localstorage 并通过 querystring (或者 body)带入接口。

也就是说,我们用 localstorage 作为持久化手段;将 querystring 作为前后端通信手段,来等效的解决 cookie 的读写问题。

值得一提的是,localstorage 同样也是分区的,而不是全局共享的,例如我直接访问 www.codesky.me 写入 localstorage 的值,和我在 xsky.me 中嵌了个 iframe,iframe 内嵌入 www.codesky.me 写入的 localstorage 值是独立的,无法交叉读取。

Storage Access

本方案通用性较高,Chrome(Chromium)、Firefox、Safari 以及移动端都支持但考虑到几个 Chromium 内核浏览器都是 2023 年十月左右的版本加入的,所以建议增加兼容性代码。此外,Webview 不一定兼容,根据 MDN 的说法,2023-12-05 才加入。

如果你用过浏览器的其他 API,Storage Access 相比之下就很好理解,简单的来说就是先问「我能不能用」,如果获得了用户的授权,接下来申请的站点就能正常使用 Cookie 了,这一部分 Cookie 甚至可以是未分区的 Cookie,也就是可以在授权站点内达成形如「未禁用第三方 Cookie」所表现的效果。

Storage Access 使用上来说体验会有一些劣化,正如上面说的,他需要用户先进行交互才能正常调用 document.requestStorageAccess()(简单的来说就是不能自动触发,需要用户点个按钮才能正常触发事件)。你还可以通过 hasStorageAccess() 来检测用户是否已经授权。这两个 API 都需要在 iframe 内部才能触发。

这一授权是站点到站点的,也就是即使你内嵌的是 www.codesky.me,之后换成了 m.codesky.me,授权仍然有效。

需要注意一点,只有 iframe 加上了以下属性才能成功触发事件:

  • 必须授予 allow-storage-access-by-user-activation 权限才能访问 Storage Access API。
  • 必须授予 allow-scripts 权限,才能使用 JavaScript 调用该 API。
  • 必须授予 allow-same-origin 权限,才能允许访问同源 Cookie 和其他存储空间。
ini 复制代码
<iframe sandbox="allow-storage-access-by-user-activation
                 allow-scripts
                 allow-same-origin"
        src="..."></iframe>
28633.png
21581.png

当然,这并不意味着每次你都需要让用户点击按钮才能继续,在授权一次后接下来有一定时间的保质期,保质期内的规则是:

  • 默认同意,不需要再次手动选择
  • Chrome 和 Firefox 之后可以允许静默调用 document.requestStorageAccess(),而 Safari 需要每次都进行互动(也就是每次访问都得点一下授权)

当然,接下来我们会将一个叫相关网站集的概念,有了它就可以很好的简化这一授权流程:

72358.png

另外,刚刚我们其实也提到了,这两个 API 只能用于 iframe 中,Chrome 更进一步提供了顶级页面用的 API:requestStorageAccessFor(),*如果跨网站请求包含 CORS 或跨域属性,则这些请求将包含 Cookie。这样可以一定程度上解决一些站点前后端跨站的问题 ,但问题是------这个 API 兼容性过于离谱(其实就是 Chrome 新加的),如果需要进一步了解,可以参考:Document: requestStorageAccessFor() method

这个方案更多可以阅读:Storage Access API

相关网站集

刚刚我们也提到了相关网站集这个概念,相关网站集相当于你有一堆看上去毫不相干的网站(都是不同站),比如 codesky.mexsky.me,但实际上都是你的网站,你也希望他们之间能够互相共享,拿 Chrome 官方的例子就是,首先你需要有一份配置文件,形如:

json 复制代码
{
  "primary": "https://primary.com",
  "associatedSites": ["https://associate1.com", "https://associate2.com", "https://associate3.com"]
}

然后把这份 JSON 提交给 Google 的 GitHub 仓库(这步属实魔幻行为),提交成功后就可以作为「相关站点」处理,然后你在调用上面提到的 document.requestStorageAccess 或者 requestStorageAccessFor 就可以尽享特权了------没错,费了半天劲,结果还是需要唤起他俩 API。

而且提交给 Chrome 的仓库,怎么想都不会是一个标准化行为,如果其他浏览器要效仿标准,难道要维护一份 Firefox 版和一份 Safari 版吗?因此这个方案看上去更迷了。

CHIPS 这个方案是我目前大力推荐的一种改造形式,因为他改造成本小,在 iframe 上作为解决方案效果较为显著,只需要注意一些 corner case 就能很好的解决这一限制。

CHIPS 也就是分区的 Cookie,用官方出品的图比较好理解分区 Cookie 这个东西:

17052476225833.jpg
17052476000888.jpg

未分区时 C 的 Cookie 无论是嵌套在 A 站点还是 B 站点都可以读取,而有了分区(Partitioned) Cookie 后,A 站点嵌入 C 和 B 站点嵌入 C 之间读取的 Cookie 是无法共享的,如果用 DB 的概念来说,其实就是加上了联合索引:

16731.png

这样你就能简单理解什么是 Partitioned Cookie 了,在浏览器支持上,目前 Firefox 和 Safari 都是不支持的。(但 Firefox 宣称他们对所有的第三方 Cookie 默认都是分区的,只是 Chrome 觉得这个方法不好,可能会带来不必要的麻烦/Bug;而 Safari 中是真没办法,还得用 Storage Access API 申请。)

但虽然不支持,如果你在低版本浏览器或者不支持的浏览器中使用,也不会产生什么副作用,只会默认忽略这个 Partitioned 标记罢了。

ini 复制代码
Set-Cookie: __Host-example=34d8g; SameSite=None; Secure; Path=/; Partitioned;

如果是不支持 Partitioned 的浏览器,会将其忽略,也就是:

ini 复制代码
Set-Cookie: __Host-example=34d8g; SameSite=None; Secure; Path=/;

Chrome 新版虽然禁用了第三方 Cookie,但如果你分区了,就可以正常在 iframe 内读写隔离版 Cookie 了。

这个方案就是这么简单吗?------对,就是这么简单,这也就是为什么我(以及 Google)都拿这个方案出来说的原因。

当然,实际改造中需要对读写进行一些处理:从优化体验的角度以及 Google 的建议,你不应该默认无脑写分区 Cookie(这个论点可以参考Partition all third-party cookies by default),尤其是在顶级站点的访问上。也就是说理论上一个良好的体验是需要你按需写入 Cookie 或者双写 Cookie 的。双写可以更好的让那未被灰度到 99% 的用户保证原来的访问体验,而 1% 的用户使用 Partitioned 版本。

但所谓双写一时爽,如果你只是普通的对 Cookie 进行读写似乎没什么问题,如果你在同一个 Cookie 中进行 Append 操作,也就是 Cookie 写入不幂等的情况,可能就会出现一些尴尬的情况了,场景可以类似于一个加减法:

99264.png

也就是说,不幂等会导致 Cookie 不一致,在未禁用第三方 Cookie 那 99% 的用户场景下可能会带来灾难性的后果------因为我不知道读的是哪一个。

特别需要注意的是:尽管在前端可以看到两个 Cookie 一个带 Partitioned 标,一个不带,但是在后端只能收到 Cookie 的 key 和 value,所以对于后端来说,这只是普通的两个同名 Cookie。(RFC 中提到:客户端实际上有排序规则,但服务端不应该依赖排序)而笔者读了几种语言的标准 cookie 库,通常本质都是构造一个 Map,或者直接以分号和等号作为分隔符进行切割并循环遍历,本质上都是取第一次出现或者最后一次出现作为有效 Cookie,而忽略其他结果。),目前几乎所有的标准库实现对倾向于取一个(也可能是因为 RFC 中提到客户端如果收到多个同名 Cookie 只取一个,而库之间尽可能对齐带来的现象)。

如果对此抱有疑问,可以通过检查后端库或者观察目前站点发送同名 Cookie 的表现来预测行为是否会存在异常。

而不同于上面的兼容性处理,对于这一方案来说,更重要的是「降级方案」,也就是如果 Partitioned 实现过程中出了问题,我们是不是只能让用户去清除他们的 Cookie------实际上我们可以通过写入 max-age=0 的 Cookie 来清除 Cookie,或者使用 Clear-Site-Data 来清除 Cookie,这一点在 Chrome 对于 CHIPS 的提案中有所提及。

再次说明,这个方案相当简单粗暴好用,但解决不了 Safari 第三方 Cookie 的问题。

共享存储空间

共享存储空间(Shared Storage)可以提供一系列 API 帮助你实现未分区的跨站存储,这样就可以和以前使用 Cookie 那样使用它了。

虽然什么方案都会比相关网站集方便,但这个 API 高度依赖 fencedframe,而这个 HTML 标签甚至没有一个属于自己的 MDN 页面,给这个方案带来了一些「面向未来」的感觉,笔者并不推荐,研究的也比较少,如果仍想了解,可以参考:

JWT / Access Token

最后说了半天,前面的解决方案好像大部分更多着眼于 iframe 中,还是不太能解决 SSO 场景中的问题,对于 SSO 来说,流程可以简单看做:

33233.png

登录步骤从原来的写 Cookie 变成写 localstorage,从某种程度上来说也是从后端写变成了生成页面前端写(后端返回 HTML 限时复刻)。最后再跳回原始页面。

其中,Ticket 是一次性的;而 Token 是持久化的,具有一定时间的保质期。

当然,也可以是 callback 直接带着 ticket 跳回原始页面,原始页面收到 ticket 后自己换 token 写入 localstorage 进行持久化(使用 localstorage + querystring 是我们最早提出的方案)。

而对于登录态的判断上,也从原来的直接调用接口变成了需要从 localstorage 取值,或者拿着 ticket 去换 token 再鉴权。(注意,这里 ticket 必须马上被消费掉,避免泄露后被利用)

58106.png

这套方案最麻烦的地方可能是,即使 Cookie 中用户数据和 Token 的算法进行了对齐,值都一样了,但是后端还是取不到 Cookie 值,需要前端显式的传入,而在显式传入的过程中,必然涉及到后端的读取改造,这一方案对于整站的回归成本极大。

另外需要注意的是,建议不要直接把 Token 放在 querystring 中,这相当于在 URL 中写明:username=sky&password=123456 这么露骨,可以选择放在 Header 或者 Body 中,更为安全。

这一方案的优点是,绝对通用,一次改造,终生受益,没有任何兼容性烦恼;缺点是,改造成本大到多数站点会退避三舍。

本身想把这个方案称为 JWT,但实际上他只是一个普通的 Access Token,而加密算法和内部实现还是有很多灵活空间的。如果真的采用 JWT,登出问题可能会变成另一个新的问题。

总结

尽管第三方 Cookie 已经禁用有一周多了,各种文章也同步出台,但是很少看到有具体完善的解决方案,而本文借用相当长的篇幅去介绍多种解决方案,所以写作成本比较高,收集了大量资料和花了好几个小时才写好(其实是拖更了一周多 emmm)。

本文确实有点长,感谢大家耐心的读到结尾,如果有对于第三方 Cookie 其他方案的想法,也欢迎留言讨论。

最后,最近有个小想法,之后更新的时候想更新一些自己的「想法」而不是「实践」,因为「实践」写作/验证周期比较长,而「想法」较为天马行空,更偏向「脑洞」,更新起来比较容易。最近有很多杂七杂八的感慨,如果大家有兴趣,之后我会开始写更短篇幅的想法类分享。

Reference

本文文章中链接较多,Reference 不代表全部引用文章。

相关推荐
mywpython15 小时前
mac 最新的chrome版本配置selenium的方式
chrome·python·selenium·macos
獨枭1 天前
Linux 下安装和使用 Jupyter Notebook
linux·chrome·jupyter
日升2 天前
Chrome 134 版本开发者工具(DevTools)更新内容
前端·chrome·浏览器
牢鹅出海2 天前
2月近10万款应用被谷歌下架,游戏、社交、工具类App遭受重创
google
牢鹅出海2 天前
口气了解Google Play审核机制,科学应对"谷歌卡审"危机
google
我要升天!3 天前
Linux中《环境变量》详细介绍
linux·运维·chrome
muzidigbig5 天前
Chrome(Google) 浏览器安装Vue2、Vue3 Devtools插件方法
chrome·vue.js devtools·google vue插件方法
pitt19976 天前
Chrome 开发环境快速屏蔽 CORS 跨域限制!
chrome·跨域·cors·解决跨越技巧
skywalk81636 天前
自动化浏览器的测试框架playwright 支持多种浏览器Chromium、Firefox 和 WebKit
前端·chrome·自动化·测试·playwright
亿牛云爬虫专家7 天前
Headless Chrome 优化:减少内存占用与提速技巧
前端·chrome·内存·爬虫代理·代理ip·headless·大规模数据采集