跨域请求携带Cookie全解析:SameSite、CORS与前端配置指南
2026/9/16 6:54:48 网站建设 项目流程

跨域请求携带 Cookie,这是我见过最容易翻车的问题之一。很多朋友本地联调一切正常,代码一上线就发现“登录态丢了”“接口 401 了”,第一反应是后端跨域配置没弄好,结果折腾半天发现根本不是那么回事。还有人把请求从 HTTP 切到 HTTPS,Cookie 突然就没了,或者换了高版本 Chrome 就不带 Cookie 了,这类问题十有八九都出在浏览器对跨域 Cookie 的管控机制上。

如果你在做对接第三方登录态、单点登录,或者写自动化脚本需要保持会话,都会遇到这个坎。今天我把跨域请求自动携带 Cookie 的完整条件拆开讲一遍:前端要开什么开关、服务端要配什么响应头、Cookie 自己又要满足什么条件,以及最常见的那几个坑到底是怎么踩出来的。

1. 跨域Cookie为什么带不上:先把原理捋清楚

1.1 同源策略和第三方Cookie:两套规则别混为一谈

先说一个最常见的误区:很多人觉得“跨域请求不带 Cookie”就是因为同源策略。这话只对了一半。CORS 同源策略管的是“浏览器能不能让你页面里的 JS 读取到跨域响应内容”,而 Cookie 带不带,还要看另一套规则:Cookie 的归属和第三方 Cookie 管控。

Cookie 不是跟页面绑定的,而是跟域名绑定的。浏览器在发请求的时候,会根据“请求的目标地址”去匹配本地存的 Cookie,只有 Domain、Path、协议等属性都对得上,才会带上。所以“跨域请求无法带 Cookie”这个说法其实不严谨——从 a.com 的页面去请求 b.com,如果 b.com 确实在浏览器里种过匹配的 Cookie,理论上这个 Cookie 是会跟着请求发出去的,前提是浏览器允许这种“第三方场景”发送。

这里引出了第三方 Cookie 的概念。对你当前打开的页面 a.com 来说,b.com 给浏览器种的 Cookie,就是第三方 Cookie。浏览器对第三方 Cookie 的管控非常严,尤其是 Chrome 和 Edge 高版本。而如果页面是 a.example.com,请求是 b.example.com,虽然对 CORS 来说是跨域,但在 Cookie 的 SameSite 判定里,它们都属于 example.com 下,属于 same-site,不会触发第三方 Cookie 拦截。这就是为什么“跨域”和“跨站”必须分开理解。

1.2 一个跨域Cookie请求能成功,要过三道关

假设页面在 a.com,接口在 b.com,你想让浏览器在跨域请求里自动带上 b.com 域下的 Cookie,整个链路要同时满足下面这些条件:

  • 前端请求必须显式声明“我要带凭证”,XHR 是withCredentials = true,fetch 是credentials: 'include'
  • 服务端响应头必须包含Access-Control-Allow-Credentials: true,同时Access-Control-Allow-Origin不能是*,必须明确指向具体来源。
  • 请求要带的 Cookie,它的Domain必须匹配请求目标 b.com,Path也得覆盖请求路径。
  • Cookie 的SameSite属性要允许跨站发送,通常得是SameSite=None,同时还要带Secure,也就是要求 HTTPS。
  • 如果 Cookie 是会话级、没过期时间,浏览器一关就没了,那你看到的“失效”其实是正常丢失。
  • 浏览器本身的隐私设置不能主动拦截第三方 Cookie。
  • 如果 Cookie 带Secure属性,页面和接口还必须跑在 HTTPS 下。

这一串条件环环相扣,任何一环断了,表现就是“浏览器没带 Cookie”。下面我逐一展开讲,每一条都配上实际配置和排查方法。

2. 前端必须打开的开关:credentials配置不当等于白干

2.1 XHR、fetch、axios里怎么设withCredentials

先看最基础的 XMLHttpRequest。很多老项目还在用 XHR,写法是:

const xhr = new XMLHttpRequest(); xhr.open('GET', 'https://b.com/api/user'); xhr.withCredentials = true; xhr.send();

withCredentials这个属性默认是false。只要不设成true,浏览器发跨域请求时就不会带 Cookie,哪怕 Cookie 本身条件全满足。这个东西没有全局开关,所以你需要在每次请求前设置,或者封装在公共请求函数里。

如果你用的是 fetch,对应的字段叫credentials

fetch('https://b.com/api/user', { credentials: 'include', });

fetch 的默认值是same-origin,也就是同源请求时带 Cookie,跨域请求默认不带;改成include后,跨域请求也会带上 Cookie。还有一个值omit,任何情况下都不带,一般用不到。

axios 在浏览器底层还是 XHR,所以你要设的是withCredentials

// 单个请求 axios.get('https://b.com/api/user', { withCredentials: true, }); // 全局默认 axios.defaults.withCredentials = true;

我之前见过不少人只在登录接口配置了withCredentials,后面的业务接口全是默认值,结果登录成功之后,后续请求全都不带会话,后端每个接口都返回 401。排查这类问题,第一步就是全局搜一下withCredentials到底配了哪几个地方。

2.2 前端配置里容易踩的坑:别靠手塞Cookie

一个经常被误解的点是:既然浏览器不自动带,那我手动在请求头里加一个Cookie字段行不行?不行。浏览器把Cookie列为forbidden header name,也就是 unsafe header,JS 里设置它会被直接忽略,控制台还会报错:Refused to set unsafe header "Cookie"。Cookie 的携带是浏览器的自主行为,只能通过开关来控制,不能由 JS 手动指定。

另外,fetch 的credentials: 'include'不能和mode: 'no-cors'一起用,否则会直接抛异常。原因是 no-cors 模式本身就是一种“降级模式”,浏览器不允许在这个模式下做带凭证的请求,这是安全底线。

还有一个细节:如果你的项目里用了 jQuery,$.ajax对 CORS 的支持也是通过xhrFields配置:

$.ajax({ url: 'https://b.com/api/user', xhrFields: { withCredentials: true } });

jQuery 默认不带凭证,这和历史遗留代码配合跨域后就会出问题。如果你在维护老系统,先搜一下项目里有没有类似的$.ajax调用,比在服务端加一堆响应头要省事得多。

3. 服务端响应头:Allow-Credentials与Origin的配合关系

3.1 为什么Access-Control-Allow-Origin不能是"*"

前端把withCredentials打开之后,浏览器不会立刻就把 Cookie 发出去,它还要先检验服务端的响应头是否允许这种“带凭证的跨域请求”。前面说了,服务端必须返回Access-Control-Allow-Credentials: true,而且这个头必须和Access-Control-Allow-Origin配合。

Access-Control-Allow-Origin如果配成*,再加上Access-Control-Allow-Credentials: true,浏览器会直接拒绝这次跨域请求。道理很简单:既然允许携带凭证,那就必须明确告诉浏览器“我只信任这些具体的来源”,如果对所有域名都开放带凭证访问,那等于把用户的登录态暴露给任意恶意网站。这是浏览器层面的硬性安全限制,不是后端想不想配的问题。

所以生产环境里,这个 Origin 要写明确:

Access-Control-Allow-Origin: https://a.com Access-Control-Allow-Credentials: true

有些项目为了省事,会动态回显请求头里的Origin,比如 Nginx 里写add_header Access-Control-Allow-Origin $http_origin;,这样任何来源都能通过,但带凭证的接口这么做风险很高。我在实践中一般会在服务端做一个白名单,把线上、预发、测试环境的域名都列进去,其他源一律不带凭证。

3.2 后端框架和Nginx里怎么配置

服务端如果用的是 Express,配合cors中间件最直接:

const cors = require('cors'); app.use(cors({ origin: 'https://a.com', credentials: true, }));

如果服务端用的是 Spring Boot,可以在方法上或者类上写@CrossOrigin

@CrossOrigin(origins = "https://a.com", allowCredentials = "true")

如果项目部署在 Nginx 后面,跨域头通常由 Nginx 直接补:

location /api/ { add_header Access-Control-Allow-Origin https://a.com; add_header Access-Control-Allow-Credentials true; add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS"; add_header Access-Control-Allow-Headers "Content-Type, Authorization, X-Requested-With"; if ($request_method = OPTIONS) { return 204; } }

这里要注意两点。第一,add_header指令在 Nginx 里不是无脑叠加,如果某个 location 里存在add_header,Nginx 会忽略继承自上一层的add_header,所以最好把该配的头都在同一处写完。第二,如果请求会返回 4xx 或 5xx,某些add_header的响应头可能不会出现在错误响应里,保险起见可以加always参数,比如add_header Access-Control-Allow-Origin https://a.com always;,否则遇到 401/403 这种状态码,CORS 头丢了,浏览器还是会拦截,前端看到的依然是跨域错误。

3.3 预检请求OPTIONS也不能马虎

跨域请求分两种:简单请求和预检请求。简单请求的要求比较苛刻,必须是用 GET、POST、HEAD 方法,并且 Content-Type 只允许application/x-www-form-urlencodedmultipart/form-datatext/plain。只要你的请求带了application/json的 Content-Type,或者塞了自定义请求头,浏览器就会先发一个OPTIONS预检请求,确认服务端允许之后,再发真正的业务请求。

预检请求本身一般不会携带 Cookie,但服务端必须在OPTIONS响应里正确返回Access-Control-Allow-MethodsAccess-Control-Allow-Headers。如果预检没通过,真实请求根本不会发出去,自然也不会带 Cookie。所以排查问题的时候,看到 Network 面板里只有一个OPTIONS请求,没有后续的 GET/POST,那问题大概率出在预检响应头配置不全,而不是 Cookie 本身。

4. Cookie自己的门槛:SameSite、Secure和Domain决定了命运

4.1 SameSite属性:高版本浏览器不带Cookie的元凶

如果你确认前端开关开了、服务端头也配了,请求头里依然没有 Cookie,那十有八九是卡在SameSite上。Chrome 80 之后,SameSite的默认值变成了Lax,这是很多跨域 Cookie 突然失效的根源,所以“Chrome 98 无法携带 Cookie”这类问题才会变成热搜。

SameSite有三个值:

  • Strict:最严格。所有跨站请求都不带 Cookie,包括用户从其他网站点链接跳转过来,浏览器也不会带。
  • Lax:默认值。只有“顶层导航”场景下的 GET 请求会带 Cookie,比如用户手动输入网址、点普通链接跳转;但 XHR、fetch、图片加载、iframe 这些跨站请求都不带。
  • None:允许跨站发送,但要求必须同时设置Secure属性,也就是只能跑在 HTTPS 下。

跨域 AJAX 要带 Cookie,SameSite=Lax是不满足条件的,你必须把 Cookie 设为SameSite=None; Secure。这里最容易踩的坑是:只写SameSite=None不写Secure,Chrome 会直接拒绝这个 Set-Cookie,响应头明明发了 Cookie,Application 面板里却看不到。

还有一种容易混淆的情况,前面提过:子域之间的“跨域”不一定“跨站”。SameSite是按 eTLD+1 来判定站点的,比如a.example.com请求b.example.com,在 CORS 里是跨域,但两个域同属example.com,属于 same-site,Lax不会拦。所以你不能一遇到跨域带不上 Cookie,就直接怪 SameSite,要先把站点关系判断清楚。

4.2 Secure、HttpOnly、Path、Domain:四个属性各自管什么

Secure属性要求请求必须走 HTTPS 才允许携带该 Cookie。你本地开发如果是http://localhost,有些浏览器会把 localhost 视为安全上下文,但一旦部署到测试环境、生产环境,地址不是 HTTPS,Secure的 Cookie 就永远发不出去。反过来说,如果线上已经全站 HTTPS,而 Cookie 没加Secure,功能虽然能跑,但安全性差一截,等于允许明文传输会话凭证。

HttpOnly经常被人误解。它的作用只是禁止 JS 通过document.cookie读写,并不影响浏览器自动发送。很多人看到 Application 面板里 Cookie 没标 HttpOnly 才放心,其实不对,HttpOnly能防的是 XSS 脚本偷 Cookie,跨域携带不受它影响。如果你的脚本里document.cookie读不到某个 Cookie,先看看是不是 HttpOnly,这个情况是正常的,不是故障。

PathDomain决定了 Cookie 的匹配范围。Cookie 只在请求 URL 的路径满足Path前缀时才会带上,如果你在/api下种了 Cookie,请求/public就不会带。Domain默认是 host-only,也就是只匹配种 Cookie 的那个主机。想要a.example.comb.example.com共享 Cookie,可以在Set-Cookie里显式写Domain=example.com;但完全不同的两个顶级域,比如a.comb.com,是没法通过 Domain 共享同一个 Cookie 的,这时候只能靠浏览器自动携带目标域的第三方 Cookie。

4.3 会话Cookie和持久化Cookie:为什么Cookie会突然失效

Cookie 还有个过期属性,很多人忽略了。如果Set-Cookie响应头里没有设置ExpiresMax-Age,那这就是一个会话 Cookie,浏览器关闭后就会清掉。用户第二天再来,登录态自然没了。像“京东签到 Cookie 总是失效”“网盘登录 Cookie 持久化”这类问题,多数都要检查两个点:一是服务端Set-Cookie时到底有没有给有效期,二是有效期是不是太短。

解决方法是Set-Cookie里带上Max-Age

Set-Cookie: session=abc123; Domain=api.example.com; Path=/; SameSite=None; Secure; Max-Age=86400

上面这个 Cookie 有效期是 86400 秒,也就是 24 小时,适合用来做短时会话。如果是“记住我”或者签到类功能,你可以把Max-Age调大,比如 30 天。但要注意,Cookie 有效期一到,浏览器就会自动删除,前端再做任何配置都没用,只能走刷新登录态的接口续期,或者让用户重新登录。

5. 实战排查:联调正常线上失败,问题出在哪

5.1 前端代理为什么会在本地“掩盖”跨域问题

先说一个很坑的场景:本地用 Vue 脚手架或者 Webpack 的 devServer 配了代理,页面请求/api,开发服务器把它转发到https://api.example.com。从浏览器的角度看,页面在localhost:5173,请求也是发到localhost:5173,这是同源请求,根本不会触发 CORS,Cookie 也就不会遇到跨域限制。所以本地联调,尤其是登录功能,往往一切正常。

结果部署到线上,前端页面跑在https://app.example.com,接口在https://api.example.com,浏览器的真实请求完全是另一回事——这是一个标准的跨域请求,所有 CORS 和 Cookie 规则都会生效。这解释了为什么很多人本地跑得好好的,上线就掉登录。如果你也想搞清楚“代理之后真实的请求地址到底是什么”,直接看 DevTools 的 Network 面板,请求行里会显示浏览器实际发出去的 URL;而服务端收到请求后,打印日志时加上 Host、Origin、Referer 字段,也能看到最终的真实来源。

代理还有一个副作用:本地开发时种下的 Cookie 都是种在 localhost 下的,生产环境域名一变,这些 Cookie 全部对不上号。本质上不是“跨域配置坏了”,而是环境变了之后,整个 Cookie 体系需要重新适配。

5.2 高版本Chrome/Edge不携带Cookie的排查顺序

用 Chrome 或者 Edge 高版本的朋友,遇到跨域 Cookie 丢失,我建议按下面这个顺序排查,别一上来就怀疑代码。

第一步,打开 DevTools 的 Application 面板,找到 Cookies,先确认目标域下到底有没有对应的 Cookie,它的SameSiteSecureHttpOnlyExpires分别是什么。如果这个 Cookie 根本不存在,后面就不用排查了,问题出在服务端为什么没种上。

第二步,切到 Network 面板,找到那个失败请求,看 Request Headers 里有没有Cookie。如果没有,说明浏览器没带。接着看 Response Headers 里有没有Access-Control-Allow-Credentials: true,以及Access-Control-Allow-Origin是不是*

第三步,看响应头里有没有Set-Cookie,如果有,再看控制台有没有相关警告。Chrome 对不符合规范的 Cookie 会在控制台打警告,比如 “This Set-Cookie was blocked because it had the SameSite=None attribute but did not have the Secure attribute” 之类,这句英文一般就是提示你SameSite=None必须配Secure

第四步,检查浏览器设置。如果用户在chrome://settings/cookies里开了“阻止所有第三方 Cookie”,那你的SameSite=None配置再对也没用,浏览器直接不给第三方 Cookie 发出去。遇到这种场景,要么引导用户放行,要么项目架构上别死磕 Cookie,改用 Token 方案。

5.3 常见问题速查表

我把实际开发中遇到的高频问题整理成了一个表格,方便你对着排查。

现象可能原因处理思路
请求头里没有Cookie前端没开withCredentials/credentials全局搜索确认配置覆盖所有跨域请求
请求头里没有Cookie,且前端口开了SameSite默认Lax拦截设置SameSite=None; Secure
响应头有Set-Cookie但Application里看不到Secure属性缺失补上Secure,确保页面和接口在HTTPS下
本地代理正常,线上掉登录代理掩盖了跨域问题按生产域名结构重新验证CORS和Cookie
服务端Allow-Origin是*,请求被拦截*与Allow-Credentials冲突改成明确的白名单Origin
Cookie自动消失,过几天失效会话Cookie或过期时间太短Set-Cookie时加Max-Age并合理设置
document.cookie读不到HttpOnly限制属正常,浏览器仍会自动发送
浏览器换一个就正常第三方Cookie策略差异检查隐私设置,或按浏览器分别验证

踩过几次坑之后,我的体会是:跨域带 Cookie 这种事,能自己控制的只有前端开关和服务端响应头,剩下的全是浏览器的政策和 Cookie 自身的属性在起作用。排查的时候不要猜,直接按“请求头有没有带、响应头有没有种、Application 里 Cookie 属性对不对”这个顺序走,基本都能定位。同样的机制往后也会越来越严,如果项目条件允许,长远看把会话凭证从 Cookie 迁到 Token 方案,能省去不少跨域场景的麻烦。但短期内,尤其是老系统、第三方登录、单点登录这些场景,Cookie 还是绕不过去,把这套规则吃透,至少不会在浏览器升级的时候手足无措。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询