CORS 下 Cookie 为什么收不到:从 withCredentials 到 SameSite
CORS 下 Cookie 能不能生效,不只取决于 withCredentials,还取决于服务端 CORS 头、Cookie Domain、SameSite、Secure 和浏览器第三方 Cookie 策略。
很多年前排查过一个问题:前端页面通过 CORS 请求后端,后端希望前端把响应里的某个字段写入 document.cookie,再在下一次请求里带回去。前端确实写了 Cookie,Chrome DevTools 里也能看到,但后端就是收不到。
当时的结论很简单:Cookie 是按域存储和发送的,页面脚本写下的 Cookie 属于当前页面所在的域,不会因为一次跨域请求就变成接口域名的 Cookie。后端应该用 Set-Cookie,而不是把 Cookie 值塞进 response body 让前端手工写。
English version: Why Cookies Fail in CORS: From withCredentials to SameSite
这个结论今天仍然成立,但已经不够完整。现代浏览器补上了 SameSite 默认值、SameSite=None 必须搭配 Secure、第三方 Cookie 限制、分区 Cookie 等机制。现在排查 CORS 下 Cookie 收不到,不能只盯着 withCredentials。
先区分三个概念
讨论 CORS 和 Cookie 时,最容易混在一起的是这三个概念:
- 同源:scheme、host、port 都相同。
https://www.example.com和https://api.example.com不同源;http://localhost:3000和http://localhost:63342也不同源。 - 同站:通常看 scheme 加可注册域名。
https://www.example.com和https://api.example.com通常同站;https://example.com和https://other.com不同站。 - Cookie 作用域:由设置 Cookie 的 host、
Domain、Path等属性决定。端口不是 Cookie 作用域的一部分。
CORS 管的是“一个 origin 的脚本能不能读取另一个 origin 的响应”。Cookie 管的是“请求某个 host/path 时,哪些 Cookie 会自动带上”。SameSite 管的是“当前请求是不是跨站,跨站时 Cookie 还能不能带”。
这三个判断维度不同,所以会出现一些看起来反直觉的场景:
localhost:63342请求localhost:3000:不同源,需要 CORS;但 Cookie 的 host 都是localhost,调试时容易在同一个 Cookie 面板里看到。www.example.com请求api.example.com:不同源,需要 CORS;但通常同站,SameSite=Lax不一定会拦住 Cookie。app.example.com请求api.other.com:不同源且不同站,既要 CORS,也会受到 SameSite 和第三方 Cookie 策略影响。
一条能工作的 CORS Cookie 链路
前端要明确带凭据。Fetch 默认只在同源请求里带 Cookie,跨源请求需要 credentials: 'include':
await fetch('https://api.example.com/me', {
method: 'GET',
credentials: 'include'
});
如果使用 XHR 或 axios,对应的是:
xhr.withCredentials = true;
axios.get('https://api.example.com/me', {
withCredentials: true
});
服务端也要明确允许带凭据。关键点是:
Access-Control-Allow-Origin必须是明确的 origin,不能是*。Access-Control-Allow-Credentials必须是true。- 如果按请求的
Origin动态返回Access-Control-Allow-Origin,要加Vary: Origin,避免缓存污染。 - 预检请求
OPTIONS不会带 Cookie,但预检响应仍要告诉浏览器后续真实请求是否允许带凭据。
一个 Express 示例:
const allowList = new Set([
'https://www.example.com'
]);
app.use((req, res, next) => {
const origin = req.headers.origin;
if (allowList.has(origin)) {
res.setHeader('Access-Control-Allow-Origin', origin);
res.setHeader('Vary', 'Origin');
res.setHeader('Access-Control-Allow-Credentials', 'true');
res.setHeader('Access-Control-Allow-Methods', 'GET,POST,PUT,DELETE,OPTIONS');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
}
if (req.method === 'OPTIONS') {
return res.sendStatus(204);
}
next();
});
最后,Cookie 应该由接口域名通过 Set-Cookie 设置。比如页面在 https://www.example.com,接口在 https://api.example.com,两者同站但不同源:
Set-Cookie: __Host-sid=...; Path=/; HttpOnly; Secure; SameSite=Lax
这里的 Cookie 是 host-only Cookie,只会发给 api.example.com。HttpOnly 让前端脚本无法读取它,适合会话 Cookie;Secure 要求 HTTPS;SameSite=Lax 在同站请求中通常足够。
如果页面和接口是不同站,比如 https://app.example.com 请求 https://api.other.com,想让 Cookie 参与跨站请求,Cookie 至少要这样:
Set-Cookie: sid=...; Path=/; HttpOnly; Secure; SameSite=None
但这只表示 Cookie 具备跨站发送的属性,不代表一定能用。浏览器或用户设置仍可能阻止第三方 Cookie。
为什么前端写 Cookie 后后端收不到
document.cookie = 'sid=123' 写的是当前页面所在 host 的 Cookie。页面在 www.example.com,脚本就不能给 api.other.com 写 Cookie。
即使用 Domain,也只能设置当前 host 或它的父域,不能设置任意外部域。比如从 api.example.com 可以设置 Domain=example.com,让 Cookie 覆盖同一可注册域名下的子域;但不能设置 Domain=other.com。
这也是为什么“后端把 Cookie 值放在 JSON 里,让前端写到 document.cookie”通常是错误方案:
- 写出来的是前端页面域名的 Cookie,不是接口域名的 Cookie。
- 如果会话 Cookie 需要
HttpOnly,前端脚本本来就不应该能写。 Set-Cookie是浏览器特殊处理的响应头,前端 JavaScript 不能读取它;即使服务端加Access-Control-Expose-Headers: Set-Cookie也没用。
正确链路应该是:接口响应里返回 Set-Cookie,浏览器在符合 CORS、credentials、Cookie 属性和浏览器策略的前提下自动保存;后续请求再由浏览器自动带上。
SameSite 改变了很多旧经验
早期很多文章会说:CORS 配好 withCredentials 和 Access-Control-Allow-Credentials,跨域 Cookie 就能正常用。今天这句话少了 SameSite。
现代浏览器通常把未声明 SameSite 的 Cookie 当成 Lax。Lax 会在同站请求中发送,也会在用户进行顶层导航的部分跨站场景中发送,但不会为了普通跨站 fetch、XHR、iframe 子资源请求随便发送。
因此,跨站接口请求如果依赖 Cookie,一般需要:
Set-Cookie: sid=...; Path=/; HttpOnly; Secure; SameSite=None
注意两个细节:
SameSite=None必须搭配Secure。Secure意味着生产环境必须使用 HTTPS;localhost是调试例外,但不要把本地表现直接当成线上表现。
如果前端和 API 只是不同子域,优先把它们放在同一个站点下,例如:
https://www.example.comhttps://api.example.com
这种架构仍然需要 CORS,因为它们不同源;但 SameSite 压力会小很多,因为它们通常是同站请求。
第三方 Cookie 策略不能靠 CORS 绕过
CORS 头、credentials: 'include'、SameSite=None; Secure 都配对了,也仍然可能收不到 Cookie。原因是浏览器的第三方 Cookie 策略还在更外层。
MDN 在 CORS 文档里也明确提醒:带凭据的跨域请求仍然受第三方 Cookie 策略约束,服务端和前端配置无法绕过用户代理的策略。
今天至少要按浏览器分别理解:
- Safari/WebKit 很早就默认限制并阻止大量第三方 Cookie,2020 年已经进入完整第三方 Cookie 阻止阶段。
- Firefox 的增强跟踪保护会阻止一部分跟踪类第三方 Cookie。
- Chrome 在 2025 年宣布继续保留用户对第三方 Cookie 的选择,不再推出新的独立提示;但无痕模式默认阻止第三方 Cookie,用户也可以在隐私设置里关闭第三方 Cookie。
所以,不应再把第三方 Cookie 当成稳定的登录基础设施。对普通业务系统,最稳妥的是尽量避免“前端站点和登录 Cookie 所在接口站点完全不同站”的设计。
如果确实是在做第三方嵌入组件,比如 iframe 小组件、地图、客服、支付或跨站嵌入应用,可以再评估 Storage Access API、CHIPS/Partitioned Cookie 等方案。但这些方案有明确场景边界,不适合作为普通前后端分离登录的默认解法。
方案选择
按稳定性排序,我会这样选:
- 同源部署:前端和 API 放在同一个 origin,或者用 Nginx/BFF 把
/api代理到后端。Cookie 最简单,CORS 问题也最少。 - 同站不同源:例如
www.example.com+api.example.com。需要 CORS 和credentials: 'include',但 Cookie 仍在同站语义内。 - 不同站但不用 Cookie 做接口身份:开放平台、跨组织 API、移动端 API 更适合用 OAuth、短期 token、Authorization header 等方式。
- 不同站且必须用 Cookie:只有在明确知道浏览器兼容性、用户设置和嵌入场景的情况下再做,并准备好第三方 Cookie 被禁用时的降级方案。
反向代理不是“土办法”。对自己控制的 Web 应用来说,把浏览器看到的前端和 API 收敛到同一个站点下,通常比和浏览器隐私策略对抗更稳。
安全边界
Cookie 会被浏览器自动带上,这也是 CSRF 的基础。CORS 不是 CSRF 防护。一个跨站表单提交或简单请求可以发出去,只是攻击页面不一定能读到响应。
如果接口使用 Cookie 做登录态,至少要考虑:
- 会话 Cookie 使用
HttpOnly; Secure。 - 能用
SameSite=Lax就不要用SameSite=None。 - 对会改变状态的请求校验 CSRF token,或校验
Origin/Sec-Fetch-Site等请求来源信号。 - 不要把
Access-Control-Allow-Origin无脑反射所有Origin。 - 不要在带凭据的 CORS 响应里使用
Access-Control-Allow-Origin: *。
Cookie 解决的是身份自动携带,不等于请求就是可信的。
调试清单
排查 CORS 下 Cookie 收不到时,可以按这个顺序看:
- 请求是不是跨源:scheme、host、port 是否完全一致。
- 前端是否设置了
fetch(..., { credentials: 'include' })或withCredentials = true。 - 响应是否有明确的
Access-Control-Allow-Origin,且不是*。 - 响应是否有
Access-Control-Allow-Credentials: true。 - 动态 origin 是否加了
Vary: Origin。 Set-Cookie是否来自接口域名,而不是 response body。- Cookie 的
Domain、Path是否覆盖了下一次请求的 URL。 - 跨站请求是否设置
SameSite=None; Secure。 - 生产环境是否是 HTTPS。
- 浏览器是否阻止了第三方 Cookie。
- DevTools 的 Network 请求里是否显示 Cookie 被 blocked,以及 blocked reason。
- Application 面板里 Cookie 所属站点是否符合预期。
Chrome DevTools 里,Network 面板点开具体请求,看 Cookies 子面板通常比只看 Headers 更清楚。被 SameSite、Secure、Domain、第三方 Cookie 策略拦掉的 Cookie,往往会在这里或 Issues 面板里给出原因。
总结
CORS 下 Cookie 能不能生效,取决于一整条链路:
- 前端要允许带凭据。
- 服务端要明确允许对应 origin 携带凭据。
- Cookie 要由目标域名通过
Set-Cookie设置。 - Cookie 的 Domain、Path、SameSite、Secure 要匹配请求场景。
- 浏览器第三方 Cookie 策略不能把它拦掉。
2017 年那次问题的根因是“前端不能替后端域名写 Cookie”。今天再补一句:即使 Cookie 是后端正确设置的,也要把 SameSite、Secure 和第三方 Cookie 限制一起纳入设计。
扩展阅读
- MDN: Cross-Origin Resource Sharing
- MDN: Set-Cookie
- MDN: Using HTTP cookies
- MDN: Using the Fetch API - Including credentials
- Chrome for Developers: View, add, edit, and delete cookies
- Privacy Sandbox: Next steps for Privacy Sandbox and tracking protections in Chrome
- WebKit: Full Third-Party Cookie Blocking and More
- MDN: Cookies Having Independent Partitioned State