经典案发现场:
你(前端,抓狂中):
"后端大佬!我 fetch
加了 credentials: 'include'
, axios
也开了 withCredentials
,为啥 Cookie 还是没带上啊?😭"
后端(一脸无辜):
"我响应头 Access-Control-Allow-Credentials: true
也给了啊,Access-Control-Allow-Origin
也指定你家域名了,咋回事?"
... 半小时的互相"甩锅"后,终于发现:Set-Cookie
里 SameSite
设成了 Lax
,被 Chrome 默默拦截了!
别慌!
今天咱们就化身技术侦探,把跨域 Cookie 从"发送"到"接收"这条路上的所有关卡、陷阱、隐藏规则,一次性扒个干净!让你从此告别"Cookie去哪儿了"的灵魂拷问。
一、背景:为啥跨域带个Cookie这么"矫情"?
想象 Cookie 是张 VIP 通行证。浏览器这个"保安大队长"有个铁律:同源策略 ------ 只有协议 (http/https
)、域名 (a.com
)、端口 (8080
) 完全一致,才算"自己人",才能自由带证进出。
一旦有不同(比如 http://a.com
访问 https://api.a.com
或者 http://b.com
),就是"跨域"。为了安全(防 CSRF 攻击)和隐私(限制第三方追踪),浏览器又加了两道超级安检门:
- CORS (跨域资源共享):后端说了算!"你(前端)想带通行证(Cookie)?行,但得先问问我(后端)同不同意,同意的凭证(响应头)得给我看!"
- SameSite / Partitioned 属性:浏览器说了算!"就算后端同意了,你这通行证(Cookie)本身也得符合我的安全规定(属性设置)才能放行!"
任何一道安检没通过,你的 Cookie 就像人间蒸发,浏览器还常常不给明确报错! 这就是为啥它这么让人头大。
二、全流程图解
一张图理清 Cookie 的跨域之旅
js
[你的网站] 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 响应头 (缺一不可!)
js
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 的"出生证明")
js
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)"的信号!而且不同请求库/方式,写法各异,一个字母都不能错!
请求方式 | 正确姿势 (✅) | 错误示范 (❌) / 关键点 |
---|---|---|
fetch | fetch('https://api.com/data', { credentials: 'include' }) |
不写 credentials 或写成 'same-origin' (只带同源 Cookie) |
axios | axios.get('https://api.com/data', { withCredentials: true }) |
拼错 withCredentials (如 withCredential 少个 s ),或 true 写成小写 true (JS 没事,但易错) |
XMLHttpRequest | const 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 等属性限制。 |
📌 核心口诀: fetch
用 include
,axios
要 withCredentials
,xhr
认准 withCredentials
属性且注意设置时机! 表单和资源标签记得加 crossorigin="use-credentials"
。
特别注意: withCredentials
是一个特殊的头,和baseURL
同一层级设置,其配置后在浏览器请求头的面板中是不可以见的
。不能直接在header头中配置,否则cookie不会携带、设置cookie也不会成功。

五、调试宝典:Cookie丢了?按步骤"破案"!
当 Cookie 神秘失踪时,别慌!拿起浏览器开发者工具 (DevTools),化身福尔摩斯,按顺序排查:
🔍 第一步:检查 Cookie 是否被成功"存进银行"(Application 面板)
- 打开 DevTools -> Application 标签页。
- 左侧选择 Storage -> Cookies。
- 在域名列表中找到你的 目标 API 的域名 (如
api.data-service.com
)。 - 查看你期望的 Cookie (如
sessionId
) 是否存在。
js
* **不存在?** 问题大概率出在**后端 `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 面板)
- 在 Network 标签页中,找到你怀疑没带 Cookie 的那个 具体请求。
- 选中该请求,查看右侧的 Headers 选项卡。
- 展开 Request Headers 部分。
- 仔细找,看有没有
Cookie: your_cookie_name=value...
这一行。
js
* **有!** 恭喜!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也会设置失败
:
设置成功
的情况下,浏览器面板中会有两条记录,如下图(设置):
设置成功
的情况下携带cookie,如下图(携带):
🔍 第三步:特别关注 - 预检请求 (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。预检失败是跨域问题的另一大常见原因!
- 必须包含
六、小结速查表:关键点一网打尽!
环节 | 关键动作 | 避坑要点 |
---|---|---|
后端 CORS | Access-Control-Allow-Origin: <精确来源> + Access-Control-Allow-Credentials: true |
绝对不能用 '*' ! |
后端 Set-Cookie | Domain=<正确域>; Secure; SameSite=None; Partitioned (Path/HttpOnly 按需) |
Domain 别乱写;SameSite=None 必须 配 Secure ;Partitioned 是未来必备! |
前端携带凭证 | 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 想象成一次跨国快递:
-
发件人 (后端):
- 准备合规包裹 (
Set-Cookie
) :地址 (Domain
) 邮编 (Path
) 要准,贴好"易碎品"(HttpOnly
)、"空运标"(Secure
)、"海关申报单"(SameSite=None
+Partitioned
),写好有效期 (Expires/Max-Age
)。 - 开具通关许可 (
CORS Headers
) :明确指定收件国 (Allow-Origin
),声明允许携带特殊物品 (Allow-Credentials: true
),列出允许的运输方式和物品清单 (Allow-Methods/Headers
)。
- 准备合规包裹 (
-
快递员 (浏览器):
- 严格安检:核对包裹信息是否合规(Cookie 属性),检查通关许可是否有效且匹配(CORS 头)。任何一项不符,直接扣留包裹(丢弃 Cookie)或退回(Console 警告)!
- 安全运输 :按规则 (
Secure
) 运送包裹。
-
寄件人 (前端):
- 正确下单 (
credentials/include/withCredentials
) :下单时必须明确勾选"寄送特殊物品/凭证" 的选项!选错或漏选,快递员根本不会去取件(不发送 Cookie)!
- 正确下单 (
只有发件人包裹包得好、许可开得对,快递员安检过得去,寄件人下单下得准,这份珍贵的 Cookie "跨国快递" 才能准确、安全地送达目的地!
另外,后端通常会通过第三方库直接配置跨域,无需像上面一样逐一配置。如:
在nestjs
中的配置(内置支持):
js
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
中的配置:
javascript
import cors from 'fastify-cors'
// 跨域白名单
app.register(cors, {
origin: [
/your-app\.com/,
/localhost/,
/\d+\.\d+\.\d+\.\d+/,
// 其它域名...
],
credentials: true
})
其它后端框架同理配置接口。
掌握这份通关手册,仔细对照每个环节,你一定能告别 Cookie 跨域丢失的烦恼!Cookie 永不迷路!🚀