Iframe 与 SameSite:Iframe 中设置 cookie 失败的原因及解决方案

目录
[隐藏]

从 Chrome 51 开始,其针对 Cookie 新增加了一个 SameSite 属性,用于防止 CSRF 攻击和用户追踪等。

而从 Chrome 80 开始 SameSite 默认值发生改变,由 None 变为 Lax,由此导致许多在网页上跳转跨站的网站页面时不会携带 Cookie,造成登录态失效等一系列问题。此外,在基于 https 的 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 标志,即使包含了也是无效的。

当在 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');
}

3 扩展参考

点赞 (0)

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注

Captcha Code