彻底搞懂Cookie跨域问题的解决方案与实战技巧

Good“思晨 安全 阅读 1,782
赞 22 收藏
二维码
手机扫码查看
反馈

又踩坑了,登录态跨不过去

上周上线前最后一天,本来以为稳了,结果测试同事甩过来一句话:你在 jztheme.com 登录完,跳到另一个子域名页面,直接 401 了。我当场一愣——这不应该是自动带 Cookie 的吗?折腾了整整一下午,总算搞明白了,记录下这个血泪过程。

彻底搞懂Cookie跨域问题的解决方案与实战技巧

一开始我以为是后端配置问题

第一反应是后端没配好 CORS。赶紧翻代码,发现已经加了 Access-Control-Allow-Credentials: true,也允许了具体域名(不是 *),按理说没问题。前端请求也加了 withCredentials: true,看起来全齐了。

但我还是不死心,怀疑是不是 domain 没设对。于是让后端同学检查 Set-Cookie 的 header,发现他们用的是:

Set-Cookie: sessionid=abc123; Path=/; HttpOnly

哦!没有指定 Domain,也没有 Secure(虽然本地开发无所谓),关键是 没开 SameSite。现代浏览器默认把 Cookie 当作 SameSite=Lax,而 Lax 不允许跨站请求携带 Cookie,即使是同主域下的子域也不行。

这里我踩了个大坑:我一直以为“同主域”就等于“同站”,但其实浏览器判定“站”(site)是根据注册域来的。比如 a.jztheme.com 和 b.jztheme.com 被认为是同一个 site 吗?是的,它们属于同一个注册域 jztheme.com,所以可以通过设置 Domain=.jztheme.com 来共享 Cookie。

但前提是 SameSite 得放开。

试了三种方案,前两个都失败了

第一个想法:把 SameSite 设成 None。简单粗暴,所有跨站请求都能带 Cookie。改后端代码:

Set-Cookie: sessionid=abc123; Path=/; Domain=.jztheme.com; Secure; HttpOnly; SameSite=None

注意这里必须加上 Secure,因为 SameSite=None 要求传输层是 HTTPS,否则浏览器会直接拒绝这个 Cookie。本地开发可以用 localhost 绕过,但一旦上预发环境就得 HTTPS。

改完后跑测试,不行。查 Network 面板,发现 response header 是对的,但 Cookie 死活不存。后来才发现 Chrome 控制台有黄色警告:“This cookie will be rejected because it has the “SameSite=None” attribute but is missing the “Secure” attribute.” 我擦,原来 nginx 反向代理那层没透传好协议头,request 到达应用时是 http 协议,导致后端误判,没加 Secure。修完代理配置才解决。

第二个尝试:干脆不用 Cookie,全部走 token 放 header。听起来合理,毕竟现在流行无状态登录。于是我让前端在登录后拿到 token 存 localStorage,每次请求手动加 Authorization: Bearer xxx

逻辑是通的,但问题来了:老系统很多地方是用 fetch 直接发的表单请求,甚至有些重定向是服务端做的,这些场景没法自动加 header。而且我们还有几个 iframe 嵌套的报表页,那些页面压根不知道外面用户的 token 是啥。

折腾半天发现兼容成本太高,放弃。

最终方案:SameSite=Lax + 主域 Cookie + 显式跳转传参

后来冷静下来想想,其实我们根本不需要完全“跨站”携带 Cookie。用户从 a.jztheme.com 点链接跳到 b.jztheme.com,这种 top-level navigation 是被 Lax 允许携带 Cookie 的。真正出问题的是什么?是 SPA 里用 fetch 请求另一个子域的接口。

比如前端部署在 app.jztheme.com,API 在 api.jztheme.com,这时候 fetch 请求就是跨站 AJAX,默认不会带 Cookie,即使 withCredentials=true,如果 SameSite 不够宽松也会失败。

所以最终决定:保持 SameSite=Lax 安全性,只允许顶级导航携带 Cookie;对于 API 请求,则通过反向代理统一到同源

具体操作:

  • 前端所有 API 请求不再直连 api.jztheme.com
  • 改为请求 /api/xxx,由 Nginx 把 /api/ 路径反向代理到真实后端
  • 这样 origin 一致,Cookie 自动带上,无需跨域处理

Nginx 配置片段如下:

server {
    listen 80;
    server_name app.jztheme.com;

    location / {
        root /var/www/app;
        try_files $uri $uri/ /index.html;
    }

    location /api/ {
        proxy_pass https://api.jztheme.com/;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

同时后端 Set-Cookie 改为:

Set-Cookie: sessionid=abc123; Path=/; Domain=.jztheme.com; HttpOnly; SameSite=Lax

注意这里没加 Secure,因为反代后内部通信可以是 HTTP。但正式环境建议始终开启 Secure。

前端代码也得改一下 fetch:

// 以前
fetch('https://api.jztheme.com/user', { credentials: 'include' })

// 现在
fetch('/api/user', { credentials: 'include' })

credentials: ‘include’ 还是要写的,不然同源请求也不会自动带 Cookie(除非是 form submit 或图片加载这类默认行为)。

还有个小问题:iframe 里的登录态

我们有个数据分析页是在 report.jztheme.com 上,用 iframe 嵌在主站里。这个 iframe 加载时需要访问 API 获取数据,但由于是嵌入上下文,浏览器认为这是跨站内嵌,SameSite=Lax 下不会发送 Cookie。

解决方案有两个:

  1. 给 iframe 页面加一个中间页,用户点击时先跳出去登录一次,再 redirect 回来(利用 top-level nav 触发 Cookie 发送)
  2. 把 report 子域也接入统一网关,让它走同源代理

我选了第二种,虽然多了一层转发,但体验更平滑。反正都是内网调用,性能损耗可忽略。

为什么不能用 document.cookie 手动传?

有人可能会想:能不能在父窗口读取 Cookie 再通过 postMessage 传给 iframe?

不行。HttpOnly 的 Cookie 根本无法通过 JavaScript 访问。这是我们故意设的,防止 XSS 窃取 session。所以这条路走不通。

总结下几个关键点

  • Samesite=Lax 已经能覆盖大部分正常跳转场景
  • Samesite=None 必须配 Secure,否则现代浏览器直接丢弃
  • Domain=.your-root-domain.com 才能在子域间共享 Cookie
  • 最稳的跨域方案其实是不跨域——用反向代理抹平差异
  • 别迷信纯前端解决方案,有时候架构调整比硬刚浏览器策略更高效

改完之后测试通过,除了个别旧接口路径映射有点麻烦外,基本没大问题。虽然代理层多了一点负担,但换来的是更安全的 Cookie 策略和更低的维护复杂度,我觉得值。

以上是我踩坑后的总结,如果你有更好的方案欢迎评论区交流。特别是那种不想动 Nginx 配置的小团队,可能得另寻出路。不过说实话,这种涉及安全的事,真别图省事。

本文章不代表JZTHEME立场,仅为作者个人观点 / 研究心得 / 经验分享,旨在交流探讨,供读者参考。
发表评论

暂无评论