从 Chrome 51 开始,其针对 Cookie 新增加了一个 SameSite 属性,用于防止 CSRF 攻击和用户追踪等。
而从 Chrome 80 开始 SameSite 默认值发生改变,由 None
变为 Lax
,由此导致许多在网页上跳转跨站的网站页面时不会携带 Cookie,造成登录态失效等一系列问题。此外,在基于 https 的 iframe 页面中设置 cookie 也会失效。
1 跨站嵌入的 Iframe 页面中设置 cookie 失效的问题
例如在页面 https://abc.lzw.me
中存在如下 iframe
:
<iframe src="https://xxx.lzw.me"></iframe>
当 htts://xxx.lzw.me
载入时,存在使用 JavaScript 设置 cookie 的操作。在之前其方法可能是这样的:
function setCookie(name, value, seconds = 0, path = '/') { let expires = ''; if (seconds) { const date = new Date(); date.setTime(date.getTime() + seconds * 1000); expires = `; expires=${date.toGMTString()}`; } document.cookie = `${name}=${escape(value)}${expires}; path=${path}`; }
上面的 setCookie
方法在页面单独从浏览器访问时执行正常,但在 Chrome 80 以后,当从 iframe 中加载时则会无效,并不会改变 docment.cookie
的值。在 iframe 中访问第三方页面时,若需要成功的设置 cookie 并在后续的 xhr 请求中携带它,则必须设置 SameSite=none
,并设置 Secure
以只允许在 https 形式的请求中可携带。以下是一个可以设置生效的简单示例:
document.cookie = 'token=467d1510-xxxx-xxxx-xxxx-73852620effa1; path=/; Secure; SameSite=None';
于是我们可以更新 setCookie
方法,参考如下:
function setCookie(sKey: string, sValue: string | number, vEnd?: number | string | Date, sPath?: string, sDomain?: string, bSecure?: boolean) { if (!sKey || /^(?:expires|max\-age|path|domain|secure)$/i.test(sKey)) return false; let sExpires = ''; if (vEnd) { switch (vEnd.constructor) { case Number: sExpires = vEnd === Infinity ? '; expires=Fri, 31 Dec 9999 23:59:59 GMT' : `; max-age=${vEnd}`; break; case String: sExpires = `; expires=${vEnd}`; break; case Date: sExpires = `; expires=${vEnd.toUTCString()}`; break; } } document.cookie = [ `${encodeURIComponent(sKey)}=${encodeURIComponent(sValue)}`, sExpires, sDomain ? `domain=${sDomain}` : '', sPath ? `; path=${sPath}` : '', bSecure ? 'SameSite=None; Secure' : '', ] .Filter(Boolean) .join('; '); return true; }
需要注意的是,通过 JavaScript 设置的 Cookie 不能包含 HttpOnly 标志,即使包含了也是无效的。
2 Iframe 中服务端返回设置的 cookie 无效问题
当在 iframe 中请求服务端认证接口并成功后,接口同时设置了认证 token 至 cookie 中。这在页面单独访问时一切正常,但当以 iframe 的形式被嵌入存在跨域的页面中时,则 cookie 设置会失效。
这也是因为没有明确的指定 SameSite
属性值。以 Node.js 开发的后端服务为例,在此之前的设置方式可能是这样的:
const https = require('https'); const fs = require('fs'); https .createServer( { key: fs.readFileSync(__dirname + '/key.pem'), cert: fs.readFileSync(__dirname + '/cert.pem'), }, (req, res) => { if (req.url === '/test-setcookie') { res.setHeader('Set-Cookie', 'token=467d1510-xxxx-xxxx-xxxx-73852620effa1; httpOnly'); res.end('test cookie'); } } ) .listen(443, '0.0.0.0');
在需要支持 iframe 嵌入的场景下,应修改为下面这种方式:
if (req.url === '/test-setcookie') { res.setHeader('Set-Cookie', 'token=467d1510-xxxx-xxxx-xxxx-73852620effa1; httpOnly; Secure; SameSite=None'); // 从请求头获取其来源站点识别是否为跨域请求,并作相关逻辑识别与约束 if (req.headers.origin) { // 注意这里不可设置为 *,必须指定为具体的域名,否则会报错 res.setHeader('Access-Control-Allow-Origin', req.headers.origin); // 允许跨域请求携带 Cookie res.setHeader('Access-Control-Allow-Credentials', 'true'); } res.end('test cookie'); }