🔥深入浅出前后端Cookie跨域

748 阅读11分钟

经典案发现场:

你(前端,抓狂中):
“后端大佬!我 fetch 加了 credentials: 'include'axios 也开了 withCredentials,为啥 Cookie 还是没带上啊?😭”

后端(一脸无辜):
“我响应头 Access-Control-Allow-Credentials: true 也给了啊,Access-Control-Allow-Origin 也指定你家域名了,咋回事?”

… 半小时的互相“甩锅”后,终于发现:Set-CookieSameSite 设成了 Lax,被 Chrome 默默拦截了!

别慌!
今天咱们就化身技术侦探,把跨域 Cookie 从“发送”到“接收”这条路上的所有关卡、陷阱、隐藏规则,一次性扒个干净!让你从此告别“Cookie去哪儿了”的灵魂拷问。


一、背景:为啥跨域带个Cookie这么“矫情”?

想象 Cookie 是张 VIP 通行证。浏览器这个“保安大队长”有个铁律:同源策略—— 只有协议 (http/https)、域名 (a.com)、端口 (8080) 完全一致,才算“自己人”,才能自由带证进出。

一旦有不同(比如 http://a.com 访问 https://api.a.com 或者 http://b.com),就是“跨域”。为了安全(防 CSRF 攻击)和隐私(限制第三方追踪),浏览器又加了两道超级安检门:

  1. CORS (跨域资源共享):后端说了算!“你(前端)想带通行证(Cookie)?行,但得先问问我(后端)同不同意,同意的凭证(响应头)得给我看!”
  2. SameSite / Partitioned 属性:浏览器说了算!“就算后端同意了,你这通行证(Cookie)本身也得符合我的安全规定(属性设置)才能放行!”

任何一道安检没通过,你的 Cookie 就像人间蒸发,浏览器还常常不给明确报错! 这就是为啥它这么让人头大。


二、全流程图解

一张图理清 Cookie 的跨域之旅


[你的网站] https://www.your-app.com
       │
       │ 1. 发起请求 (GET/POST...)
       │    ✅ 关键:前端正确设置"携带凭证"标志
       │        - fetch: `{ credentials: 'include' }`
       │        - axios: `{ withCredentials: true }`
       │        - xhr: `xhr.withCredentials = true`
       ▼
[目标API] https://api.data-service.com/user
       ▲
       │ 2. 后端响应
       │    ✅ 关键:设置正确的 CORS 响应头 + Set-Cookie
       │        - `Access-Control-Allow-Origin: https://www.your-app.com` (精确匹配!)
       │        - `Access-Control-Allow-Credentials: true`
       │        - `Set-Cookie: session=abc123; Domain=.data-service.com; Path=/; Secure; SameSite=None; Partitioned; HttpOnly`
       │
       │ 3. 浏览器安全审核
       │    ✅ 关键:Cookie 属性合规 + CORS 头匹配
       │        - 检查 SameSite, Secure, Domain, Partitioned 等
       │        - 核对 CORS 头是否允许来源和凭证
       │
       │ 4. 存储 Cookie (通过审核后)
       │
       │ 5. 下次请求自动携带 (符合 Domain/Path 规则时)
       └─── 请求头中出现:`Cookie: session=abc123`

⚠️ 重点警告: 1, 2, 3, 4, 5 任何一个环节出错,你的 Cookie 就会在某个环节“神秘消失”!


三、后端配置

如何让浏览器“安心收下”Cookie?(核心响应头 + Set-Cookie)

后端的响应是 Cookie 能否成功落地的第一步!请务必配置好这两组“通关文牒”:

1. CORS 响应头 (缺一不可!)

HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://www.your-app.com  # 绝对不能用 '*'!必须是请求来源的确切域名!
Access-Control-Allow-Credentials: true                 # 明确告诉浏览器:允许前端带凭证(Cookie)来!
Access-Control-Allow-Methods: GET, POST, PUT, DELETE   # 允许的 HTTP 方法
Access-Control-Allow-Headers: Content-Type, Authorization, X-Custom-Header # 允许前端携带的请求头
Access-Control-Expose-Headers: X-Custom-Header         # (可选)允许前端读取的额外响应头
Access-Control-Max-Age: 86400                          # (可选)预检请求缓存时间(秒)

💡 Nginx/反向代理用户请注意: 如果你用了代理,确保这些 CORS 头是最终由你的应用服务器生成,或者你在代理层(如 Nginx)正确添加了它们!代理默认可能会吞掉应用设置的头。

2. Set-Cookie 响应头 (Cookie 的“出生证明”)

Set-Cookie: sessionId=xyz789abc;
           Domain=.data-service.com;    # 必须是目标 API 的域或其父域(如 .data-service.com 包含 api.data-service.com)。写错或范围太大(如 .com)会被拒收!
           Path=/;                      # 指定 Cookie 生效的路径。通常设 / 表示全站可用。
           Secure;                      # **强制要求**:Cookie 只通过 HTTPS 传输!本地 HTTP 调试时没有它可能行,上线 HTTPS 必挂!
           SameSite=None;               # **关键钥匙**:明确声明此 Cookie 可用于跨站(跨域)请求!用 Lax/Strict 跨域请求基本带不上。
           Partitioned;                # **未来通行证**:应对 Chrome 等浏览器逐步淘汰第三方 Cookie 的趋势,使用 Partitioned 属性是新的最佳实践,确保 Cookie 在跨站上下文中可用(需要 Chrome 114+,未来更重要)。
           HttpOnly;                    # (推荐)防止 JavaScript 读取,增强安全性(防 XSS)。
           Expires=Wed, 21 Aug 2025 07:28:00 GMT; # 或 Max-Age=2592000; 设置有效期。

🚫 后端常见踩坑点:

  • Domain 写错: 写成 '.com' (范围太大无效)、'api.data-service.com' (但前端是 'app.data-service.com' 且没设父域) 等。精准匹配或合理父域是关键!
  • 漏掉 Secure 本地 http://localhost 测试可能没事,一部署到线上 https 环境,Cookie 就因为缺少 Secure 标记被浏览器拒绝存储或发送!HTTPS 环境必须加!
  • SameSite 设成 Lax/Strict 这是导致“我明明配了 CORS 为啥 Cookie 还不带?”的头号元凶!跨域请求(尤其是非导航的 POST/PUT/DELETE 或非顶级导航的 iframe 内请求)需要 'SameSite=None'!记住:跨域带 Cookie,'SameSite=None' + 'Secure' 是黄金搭档!
  • 忽略 Partitioned (未来大坑): 随着 Chrome 逐步限制第三方 Cookie,未设置 'Partitioned' 的 'SameSite=None; Secure' Cookie 在跨站 iframe 等场景下可能未来会失效尽早适配 'Partitioned' 是明智之举!

四、前端配置

如何让浏览器“心甘情愿带上”Cookie?(不同请求方式配置)

前端同学,光后端配好了还不够!你得在发起请求时,明确打上“我要带通行证(Cookie)”的信号!而且不同请求库/方式,写法各异,一个字母都不能错!

请求方式正确姿势 (✅)错误示范 (❌) / 关键点
fetchfetch('https://api.com/data', { credentials: 'include' })不写 credentials 或写成 'same-origin' (只带同源 Cookie)
axiosaxios.get('https://api.com/data', { withCredentials: true })拼错 withCredentials (如 withCredential 少个 s),或 true 写成小写 true (JS 没事,但易错)
XMLHttpRequestconst xhr = new XMLHttpRequest(); xhr.open('GET', url); xhr.withCredentials = true; xhr.send();顺序很重要! 必须在 xhr.open() 之后,xhr.send() 之前设置 withCredentials
<form> 提交<form action="https://api.com/submit" method="post" crossorigin="use-credentials">漏掉 crossorigin="use-credentials" 属性 (现代浏览器需要)
<img>/<script><img src="https://track.com/pixel" crossorigin="use-credentials">漏掉 crossorigin="use-credentials" 属性。注意:即使带了,能否成功也受 Cookie 本身 SameSite 等属性限制。

📌 核心口诀: fetchincludeaxioswithCredentialsxhr 认准 withCredentials 属性且注意设置时机! 表单和资源标签记得加 crossorigin="use-credentials"


特别注意: withCredentials是一个特殊的头,和baseURL同一层级设置,其配置后在浏览器请求头的面板中是不可以见的。不能直接在header头中配置,否则cookie不会携带、设置cookie也不会成功。

Clipboard_Screenshot_1755569654.png


五、调试宝典:Cookie丢了?按步骤“破案”!

当 Cookie 神秘失踪时,别慌!拿起浏览器开发者工具 (DevTools),化身福尔摩斯,按顺序排查:

🔍 第一步:检查 Cookie 是否被成功“存进银行”(Application 面板)

  1. 打开 DevTools -> Application 标签页。
  2. 左侧选择 Storage -> Cookies
  3. 在域名列表中找到你的 目标 API 的域名 (如 api.data-service.com)。
  4. 查看你期望的 Cookie (如 sessionId) 是否存在。
    *   **不存在?** 问题大概率出在**后端 `Set-Cookie` 或浏览器安全策略拦截**上!立刻进行:
        *   检查 **Network 面板** 中对应 API 响应的 **`Set-Cookie` 头** 是否存在且格式正确(尤其 `Domain`, `Secure`, `SameSite=None`)。
        *   检查 **Console 面板** 有没有浏览器发出的黄色⚠️警告!常见如:
            *   `This Set-Cookie was blocked because it had the "SameSite=Lax" attribute but came from a cross-site response...` -> `SameSite` 设错了!
            *   `This Set-Cookie was blocked because it had the "Domain" attribute with a value that is a public suffix...` -> `Domain` 设置范围过大无效。
            *   `This Set-Cookie was blocked because it was not sent over a secure connection and had the "Secure" attribute.` -> 本地 HTTP 测试但 Cookie 设了 `Secure`。
    *   **存在?** 太好了!进入第二步。

🔍 第二步:检查 Cookie 是否被“带出门”(Network 面板)

  1. Network 标签页中,找到你怀疑没带 Cookie 的那个 具体请求
  2. 选中该请求,查看右侧的 Headers 选项卡。
  3. 展开 Request Headers 部分。
  4. 仔细找,看有没有 Cookie: your_cookie_name=value... 这一行。
    *   **有!** 恭喜!Cookie 成功带上了!问题可能在后端处理逻辑或后续环节(但这已不属于跨域携带问题)。
    *   **没有!** 问题在于:浏览器有 Cookie,但**这次请求没把它带出来**!重点排查:
        *   **前端“携带凭证”标志**:确认你的 `fetch`/`axios`/`xhr` 配置**绝对正确无误**(回看第四部分表格)。
        *   **Cookie`Path``Domain` 匹配**:请求的 URL 路径是否在 Cookie`Path` 范围内?请求的域名是否匹配 Cookie`Domain` 或其子域?
        *   **`Secure` Cookie 发给了 HTTP?** 如果 Cookie`Secure` 标记,但当前页面是 `http://`,请求也是 `http://`,浏览器不会发送它。确保全站 HTTPS。
        *   **`SameSite` 严格限制?** 虽然你存了 `SameSite=None`Cookie,但某些非常严格的上下文(如某些 iframe 沙盒环境)可能仍有限制。检查 Console 警告。

https请求下secure无值或为false也会设置失败Clipboard_Screenshot_1755570100.png

设置成功的情况下,浏览器面板中会有两条记录,如下图(设置):

Clipboard_Screenshot_1755570313.png 设置成功的情况下携带cookie,如下图(携带): Clipboard_Screenshot_1755570544.png

🔍 第三步:特别关注 - 预检请求 (OPTIONS)

对于非简单请求(如用了 Content-Type: application/json 或自定义头 X-Whatever),浏览器会先发一个 OPTIONS 预检请求探路。

  • Network 面板找到这个 OPTIONS 请求。
  • 检查它的 Response Headers
    • 必须包含 Access-Control-Allow-Origin (你的确切来源) 和 Access-Control-Allow-Credentials: true
    • 如果请求带了自定义头,Access-Control-Allow-Headers 必须包含这些头名。
    • 如果 OPTIONS 请求失败 (状态码非 2xx),后面的正式请求根本不会发出,自然带不了 Cookie。预检失败是跨域问题的另一大常见原因!

六、小结速查表:关键点一网打尽!

环节关键动作避坑要点
后端 CORSAccess-Control-Allow-Origin: <精确来源> + Access-Control-Allow-Credentials: true绝对不能用 '*'
后端 Set-CookieDomain=<正确域>; Secure; SameSite=None; Partitioned (Path/HttpOnly 按需)Domain 别乱写;SameSite=None 必须SecurePartitioned 是未来必备!
前端携带凭证fetch: credentials: 'include'
axios: withCredentials: true
xhr: xhr.withCredentials = true
拼写/值要绝对正确!
xhr 顺序要对 (open 后, send 前)!
调试 - 存储Application > Cookies > 目标域下是否存在?
看 Console 警告!
没有?查 Set-Cookie 响应头和 Console 拦截警告!
调试 - 携带Network > 具体请求 > Request Headers > 找 Cookie: ...有存但没带?查前端凭证配置、Cookie 的 Path/Domain 匹配、是否 HTTPS 环境、SameSite 是否真 None!
调试 - 预检Network > OPTIONS 请求 > 状态码和 Response Headers预检失败 (非 2xx) 正式请求不发!检查 OPTIONS 返回的 CORS 头是否齐全正确!

七、总结:让 Cookie 跨域如丝般顺滑

把跨域 Cookie 想象成一次跨国快递

  1. 发件人 (后端):

    • 准备合规包裹 (Set-Cookie):地址 (Domain) 邮编 (Path) 要准,贴好“易碎品”(HttpOnly)、“空运标”(Secure)、“海关申报单”(SameSite=None + Partitioned),写好有效期 (Expires/Max-Age)。
    • 开具通关许可 (CORS Headers):明确指定收件国 (Allow-Origin),声明允许携带特殊物品 (Allow-Credentials: true),列出允许的运输方式和物品清单 (Allow-Methods/Headers)。
  2. 快递员 (浏览器):

    • 严格安检:核对包裹信息是否合规(Cookie 属性),检查通关许可是否有效且匹配(CORS 头)。任何一项不符,直接扣留包裹(丢弃 Cookie)或退回(Console 警告)!
    • 安全运输:按规则 (Secure) 运送包裹。
  3. 寄件人 (前端):

    • 正确下单 (credentials/include/withCredentials):下单时必须明确勾选“寄送特殊物品/凭证” 的选项!选错或漏选,快递员根本不会去取件(不发送 Cookie)!

只有发件人包裹包得好、许可开得对,快递员安检过得去,寄件人下单下得准,这份珍贵的 Cookie “跨国快递” 才能准确、安全地送达目的地!

另外,后端通常会通过第三方库直接配置跨域,无需像上面一样逐一配置。如:

nestjs中的配置(内置支持):

import { NestFactory } from '@nestjs/core';
import { NestExpressApplication } from '@nestjs/platform-express';
import { AppModule } from './app.module';
const app = await NestFactory.create<NestExpressApplication>(AppModule);
  app.enableCors({
    origin: (origin, callback) => {
      // 允许的origin
      const allowedOriginRegexList = [
        /your-app\.com/,
        /localhost/,
        /\d+\.\d+\.\d+\.\d+/,
        // 其它域名...
      ];
      // 本地调试放行
      // if (!origin) return callback(null, true);
      const isOk = allowedOriginRegexList.some((reg) => reg.test(origin));
      callback(null, isOk);
    },
    credentials: true,
    allowedHeaders: ['Content-Type', 'Authorization'],
    methods: 'GET,HEAD,PUT,PATCH,POST,DELETE,OPTIONS',
  });

fastify中的配置:

import cors from 'fastify-cors'
// 跨域白名单
app.register(cors,  {
  origin: [
    /your-app\.com/,
    /localhost/,
    /\d+\.\d+\.\d+\.\d+/,
    // 其它域名...
  ],
  credentials: true
})

其它后端框架同理配置接口。

掌握这份通关手册,仔细对照每个环节,你一定能告别 Cookie 跨域丢失的烦恼!Cookie 永不迷路!🚀


上面的示例域名是通过whistle代理配置的, 具体配置方式可以参考这两篇文章

🔥一条命令搞定全局代理!告别Whistle插件依赖的极简方案
为什么推荐使用Whistle而不是Fiddler、Charles!🤗

# 设置代理规则(知乎 代码到 百度)
# https://www.zhihu.com https://www.baidu.com

https://api.data-service.com http://localhost:3636
https://www.your-app.com http://localhost:3838