从 Chrome 51 开始,其针对 Cookie 新增加了一个 SameSite 属性,用于防止 CSRF 攻击和用户追踪等。
而从 Chrome 80 开始 SameSite 默认值发生改变,由 None
变为 Lax
,由此导致许多在网页上跳转跨站的网站页面时不会携带 Cookie,造成登录态失效等一系列问题。此外,在基于 https 的 iframe 页面中设置 cookie 也会失效。
1 跨站嵌入的 Iframe 页面中设置 cookie 失效的问题
例如在页面 https://abc.lzw.me
中存在如下 iframe
:
1 |
当 htts://xxx.lzw.me
载入时,存在使用 JavaScript 设置 cookie 的操作。在之前其方法可能是这样的:
1 2 3 4 5 6 7 8 9 | 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 形式的请求中可携带。以下是一个可以设置生效的简单示例:
1 | document.cookie = 'token=467d1510-xxxx-xxxx-xxxx-73852620effa1; path=/; Secure; SameSite=None' ; |
于是我们可以更新 setCookie
方法,参考如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | 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 开发的后端服务为例,在此之前的设置方式可能是这样的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | 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 嵌入的场景下,应修改为下面这种方式:
1 2 3 4 5 6 7 8 9 10 11 12 | 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' ); } |