当浏览器默认禁用第三方 cookie
前一阵子,我们发现高版本的 Safari 中默认会阻止第三方 cookie,如下图所示。
问题
什么是第三方 cookie 呢?在访问一个网站 A 时,网站 A 算作第一方,如果网站 A 中引用了另一个网站 X(网站 X 的域名与网站 A 的域名不同)的资源,这时这个网站 X 就被认为是第三方。需要注意的是,这儿区分不同网站的标准是域名是否相同,而不是这两个网站是否由同一个公司运营。比如,taobao.com 和 tmall.com 被认为是两个网站,尽管它们都属于阿里集团。
在网站建设中,使用第三方资源非常常见,大多数据情况下,这并不会带来问题。不过有时候,我们可能希望能读写这个第三方域下的 cookie,这时问题就来了。
比如我们有一个网站 a.com,其中埋有一段 JavaScript 脚本,每当用户打开 a.com 中的页面时,这段脚本就会发送一个 GET 请求到 x.com。这样,只需要分析 x.com 的日志,就可以了解 a.com 的访问情况。
如果只是要统计 a.com 的 PV,那么只需要将 x.com 的日志中所有记录加起来就行了,但是,如果要统计 UV 呢?
这时就需要在 x.com 这个域下写一个 cookie 用于标识当前用户,比如叫 USER_ID。当用户访问 a.com 的页面,也即发起到 x.com 的请求时,x.com 的服务器检查 x.com 域下是否有 USER_ID 这个 cookie,如果有则什么也不做,如果没有,则生成一个新的 USER_ID 并写入 cookie。有了这个 cookie 之后,分析 x.com 的日志就可以同时得到 a.com 的 PV 与 UV。整个打点过程如下图所示。
但问题是,x.com 在这儿属于第三方域,在高版本的 Safari 浏览器中,向第三方域写 cookie 受到了阻止。带来的结果就是,用户每次访问 a.com 时,发向 x.com 的请求的 cookie 都为空,于是 x.com 的服务器每次都认为这是一个新访问者,每次都生成一个新的 USER_ID 写回去,但当同一个用户再访问下一个 a.com 的页面时,发向 x.com 的请求的 cookie 仍然为空。最后,分析 x.com 的日志时就会发现,访问 PV 没有变化,但 UV 却暴涨,几乎和 PV 持平。
或许有人会问,打点服务器为什么要使用第三方域 x.com 呢?如果使用与站点相同的域 a.com 不就没有问题了吗?的确,如果打点服务器与站点同域那就没有问题了,不过很多时候我们并不能做到这一点,比如我们可能需要向很多个域名完全不同的站点提供同一套打点服务。
这个问题目前并不算严重,因为还只有高版本的 Safari 有这样的限制。但是,Safari 增加这个限制是为了保持用户隐私(因为有大量的广告站点滥用第三方 cookie),很有可能在不久的将来其他浏览器也会跟进,因此我们不得不尽早寻找解决之道。
P3P 方案在这儿也是走不通的,KISSY 的开发者承玉曾经提出过一个解决方案,简单来说,就是使用 POST 来代替 GET,这样就能继续在高版本的 Safari 中写入第三方 cookie。但遗憾的是这个方案在 Safari 5.1.4+ 的版本中失效了,估计 Safari 已经修复了漏洞。另外,Google 曾经因为使用类似的方式继续在高版本 Safari 下读写第三方 cookie 而被告上法庭,在去年 8 月时被罚款 2250 万美元,也就是说,如果继续使用各种 hack 的方式绕过浏览器限制读写第三方 cookie,有可能面临法律风险。而且随着浏览器不断升级,各种原来可用的 hack 方式也都陆续失效了。
因此,必须要寻找其他解决方案。
第二方方案
经过测试,我们发现目前 Safari 只会在第三方域下完全没有 cookie 时阻止第三方 cookie,而第三方域下只要有过任意一个 cookie,即可继续使用以前的方式顺利读写。但是,怎么才能在第三方域下写入第一个 cookie 呢?
我们测试了很多方案,包括 iframe 嵌套等,最后发现至少在 Safari 6 中,如果第三方域下完全没有 cookie,那么就没有办法向其写入 cookie,唯一的办法是将它变成第二方,也即让这个域在顶层窗口打开。也就是说,如果第三方域下没有 cookie,要向它写入第一个 cookie,要么将页面先跳到这个域,写入 cookie,再跳回来,要么弹出一个新窗口,写入 cookie,再关闭弹窗。
显然,这两种方案对用户体验来说都不好。
localStorage 方案
我们注意到,高版本 Safari 只阻止了第三方 cookie,并没有阻止第三方 localStorage,于是,我们便有了一个更为激进的方案:放弃第三方 cookie,使用 localStorage 来代替。
这个方案的本质是这样的:在 a.com 中嵌入一个 w.com 域的 iframe,这个 iframe 读取 localStorage(w.com 域下),取到各种原来需要保存在第三方 cookie 中的值,然后发送一个 GET 或 POST 请求到 x.com,原来那些记录在 cookie 中随着 HTTP 请求头发送的信息则改为通过 url 参数(GET 方式)或 Form 表单(POST 方式)的形式发送。如果要发送的内容不多,那么可以使用 GET 方式发送,只需返回一个 jsonp 即可,然后 iframe 再将 jsonp 中的数据写入 localStorage。如果需要发送的内容很多,有可能使 URL 超长,那么就需要使用 POST 方式发送,这时,需要在 iframe 中再创建一个 iframe 作为 POST 的 target,然后新 iframe 再将数据用 postMessage 等方式传回原 iframe,原 iframe 再写回 localStorage。
整个过程(使用 POST)如下图所示:
这个方案的问题是比较复杂,整个流程长了很多,需要用到一些 HTML5 特性,比如 localStorage、postMessage 等,不过好在不支持第三方 cookie 的浏览器基本上都是对 HTML5 支持良好的高版本浏览器。
就目前来看,比较保险的做法是新老方案并行,在老浏览器上继续使用第三方 cookie,在高版本 Safari 等默认阻止第三方 cookie 的浏览器上使用新方案。虽然不完美,但确实是可行的。期待不久的将来能有一种更完美的方案。
2013-03-26 更新: Firefox 开始跟进 Safari 默认阻止第三方 cookie 的策略
评论:
不知用url传参数的方法来处理,是否可行
其实要解决的问题是浏览器能记住这个用户,因此似乎必须得在本地写点什么。如果用参数传递,用户完全关闭浏览器后再次访问时(可能访问的是另一个不同域名的子站或相关站点),我们怎么知道他以前来过没有呢?
好文章,顶了
用flash
不是所有用户设备上都有flash呢,比如iOS设备……
这篇文章很好,但是我有些疑问:浏览器和应用的本地存储都在各自不同的sandbox中,Mobile Safari 本地存储的UUID,在App中是无法读取的。如果我没理解错的话,那么这个方法应该是行不通的。当然也可能是我没明白你的方法,还请指正。谢谢。
不太明白你的问题,这儿说的方案都是在浏览器中的,你说的是在移动平台上,浏览器和别的APP共享数据吗?
请问下localstorage方式中,为什么需要加载w.com的iframe,并将用户信息存储在w.com下的localstorage中,而不是直接存在a.com下的localstorage中呢?
完全可以存储在a.com下,不过我们有很多域名会共用一套打点系统,除了a.com外还有b.com、c.com ...,为了做得更通用一点,同时为了避免维护a.com的同学无意中修改或删除我们的值,所以用了一个独立的w.com来存储信息。
明白了,十分感谢!
[…] 关于第三方cookie被禁用问题,两年前季札师兄写过一篇当浏览器默认禁用第三方cookie,但是时过境迁,第三方localStorage的方案已经也被禁用了 […]