1. 跨域请求的底层逻辑
1.1 同源策略到底在保护什么
做前端开发的,几乎没有不被跨域问题折磨过的。跨域请求本身不难理解,难的是搞清楚浏览器在不同场景下的行为差异,尤其是Cookie的携带规则——明明后端CORS都配好了,请求也发出去了,可一到需要登录态的接口就傻眼,Cookie就是没带上来。
先说基础。跨域,全称是跨域资源共享(CORS,Cross-Origin Resource Sharing),它的存在根子在于浏览器的同源策略。所谓同源,指的是协议、域名、端口三者完全一致。举个例子,https://a.example.com:443 和 https://b.example.com:443 不同源,因为域名不同;http://a.example.com 和 https://a.example.com 也不同源,因为协议不同;https://a.example.com:8443 和 https://a.example.com 更不同源,因为端口不同。
同源策略本质上是浏览器的安全防线,它保护的是用户存储在浏览器里的各类数据,包括Cookie、LocalStorage、IndexedDB等,防止恶意网站跨站读取或篡改其他站点的数据。你可以把它理解成小区门禁系统:你不是这个小区的住户,保安不会让你随意进出每栋楼,更不会让你去别人家门口拿钥匙。浏览器默认信任“同源”的站点,就像物业信任本小区业主,跨域的可信度天然就打折扣。
但这里有个关键认知:同源策略限制的是浏览器的行为,HTTP协议本身并不限制跨域。你用curl或Postman访问任意域名,服务端照样响应,Cookie在请求头里该带还是带,根本没有跨域一说,因为你没有浏览器这个“门禁”。换句话说,跨域问题的本质是浏览器在替用户做安全决策,而这个决策逻辑,就是我们今天要重点拆解的部分。
1.2 跨域请求的分类与浏览器处理流程
浏览器处理跨域请求,逻辑上分为两类:简单请求和预检请求。
简单请求需要同时满足以下条件:请求方法只能是GET、POST、HEAD,其中POST的Content-Type只允许application/x-www-form-urlencoded、multipart/form-data、text/plain,且不能携带自定义请求头。满足这些条件时,浏览器会直接发出实际请求,并在请求头里自动加上Origin字段,比如:
code复制Origin: http://localhost:8080
服务端返回响应时,如果响应头里有Access-Control-Allow-Origin且值匹配,浏览器就认为这次跨域访问是合法的,把数据交给页面;如果响应头缺失或值不匹配,浏览器就会拦截响应,页面拿不到数据,控制台报错里通常能看到CORS policy字样。
预检请求则是针对“非简单请求”的。只要请求方法用了PUT、DELETE、PATCH,或者POST的Content-Type是application/json,或者带了自定义请求头,浏览器就会先发一个OPTIONS请求过去,询问服务端:我即将发出的请求方法、头、内容类型,你允许吗?服务端需要通过Access-Control-Allow-Methods和Access-Control-Allow-Headers来回答。只有预检通过,浏览器才会正式发出实际请求。
这个流程理解起来不难,但实际开发中90%的CORS报错都出在预检环节。很多人调接口发现跨域报错,第一反应是后端没配CORS,其实后端口头上都配了,只是Access-Control-Allow-Headers里漏了前端自定义的请求头字段,预检一上来就挂了,后面实际请求压根没发出去。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. Cookie携带规则与限制
2.1 浏览器的Cookie存储与发送规则
Cookie的携带限制,比跨域本身还要绕。因为即使你CORS配置正确、跨域请求成功返回了数据,Cookie也不一定会被存下来,更不一定会被带上。
首先是存储规则。Cookie按域名维度存储,服务端通过Set-Cookie响应头下发Cookie时,可以带上Domain和Path属性来控制生效范围。Domain如果不设置,默认就是当前请求的域名,且不包含子域;如果显式设置为某个顶级域,比如.example.com,则该域及其所有子域都能携带。Path控制URL路径的匹配范围,表示只有访问该路径及其子路径时才发送Cookie。
其次是发送规则。浏览器发送Cookie时,会基于以下原则决定是否携带:目标请求的域名是否匹配Cookie的Domain所属关系,以及请求路径是否匹配Path。注意,这里的匹配关系是域名从右往左的包含匹配,不是简单相等。
再说跨域场景。默认情况下,浏览器发起跨域请求时,不会自动把目标域名的Cookie放到请求头里——哪怕你之前访问过这个域名、浏览器里已经存了这个Cookie。这跟你直接访问该域名时自动携带Cookie的行为完全不同。原因很简单,跨域请求在浏览器看来是“第三方请求”,而第三方请求携带Cookie会带来严重的隐私安全问题。你在一家购物网站登录了,另一个广告网站如果也能通过跨域请求带上你的登录态,那你的账号安全就完全失控了。
所以,跨域请求想要携带Cookie,需要前后端双方都“明确同意”,缺一不可。
2.2 SameSite属性的演进与影响
在讨论跨域携带Cookie的配置之前,有一个最近几年影响很大的属性必须单独拿出来讲:SameSite。
SameSite是Set-Cookie响应头的一个属性,用来控制Cookie在跨站(Cross-Site)请求中的发送策略。它有三个值:
Strict:最严格。Cookie仅在当前站点的上下文中携带,任何跨站请求都不带。Lax:默认值。允许部分跨站请求携带,但仅限于顶级导航,比如用户点击链接从一个站点跳到另一个站点。XHR、fetch、图片请求等子资源和跨站请求一律不携带。None:允许所有跨站请求携带,但必须同时指定Secure属性,也就是只能在HTTPS连接上发送。
用表格可以更直观对比:
| SameSite值 | 顶级导航请求 | 跨站子资源请求(XHR/fetch/图片等) | 需要HTTPS |
|---|---|---|---|
| Strict | 不携带 | 不携带 | 否 |
| Lax | 携带 | 不携带 | 否 |
| None | 携带 | 携带 | 是 |
Chrome从80版本开始把默认值从None改成了Lax,这直接影响了很多传统的前后端分离项目。最典型的现象就是:你用一个前端站点去跨域调用另一个域名的后端接口,接口本身不报错,但如果后端接口依赖的是跨域场景下的Cookie(比如单点登录、第三方登录回跳),你会发现Cookie就是不带上。因为导航到目标站点时,Lax允许带Cookie,但页面里的XHR/fetch请求目标接口时,Cookie被吞了。
很多老项目线上出问题,排查了半天,最后发现就是升级浏览器后SameSite从宽松变成了限制,Cookie携带策略变了。这个坑我踩过不止一次。
2.3 跨域携带Cookie的两个必要配置
回到正题:跨域请求要携带Cookie,必须满足两个条件。
第一个条件是后端设置响应头:
code复制Access-Control-Allow-Credentials: true
这个头告诉浏览器:这个跨域请求我可以接受Cookie,你放行吧。除此之外,Access-Control-Allow-Origin的值还不能是*,必须明确指定前端的域名,比如http://localhost:8080。因为当Credentials开启时,使用通配符会让浏览器认为这是不安全的匹配。
第二个条件是前端在请求中显式开启凭据携带。原生XHR需要设置:
javascript复制const xhr = new XMLHttpRequest()
xhr.open('GET', 'https://api.example.com/user')
xhr.withCredentials = true
xhr.send()
fetch则改为:
javascript复制fetch('https://api.example.com/user', {
credentials: 'include'
})
这两个条件缺一不可。我见过很多联调场景:后端把Allow-Credentials配了,前端fetch却忘记加credentials: 'include';或者前端加了,后端Access-Control-Allow-Origin忘了从*改成具体域名,浏览器直接拦截响应。两边互相以为对方配好了,一调试就是一上午。
还有一个容易忽略的点:Allow-Origin必须精确匹配前端发起请求时的Origin,包括协议和端口。http://localhost:8080和http://127.0.0.1:8080在浏览器看来是不同的Origin,后端如果只写了其中一个,另一个就会跨域失败。
3. 实操:从零搭一个跨域带Cookie的联调环境
3.1 模拟场景与初始报错
理论讲完,用真实代码演一遍。假设我们要搭建一个前后端分离的登录场景:前端跑在http://localhost:8080,后端接口跑在http://localhost:3000。
第一步,先用Node.js写一个极简后端接口:
javascript复制const http = require('http')
const server = http.createServer((req, res) => {
res.setHeader('Access-Control-Allow-Origin', 'http://localhost:8080')
res.setHeader('Set-Cookie', 'token=abc123; Path=/; HttpOnly')
res.writeHead(200, { 'Content-Type': 'application/json' })
res.end(JSON.stringify({ code: 0, message: 'login success' }))
})
server.listen(3000)
前端页面用fetch调用:
javascript复制fetch('http://localhost:3000/api/login')
.then(res => res.json())
.then(data => console.log(data))
这个初始情况下,浏览器会正常收到响应,数据也能打印出来,因为后端允许了跨域源。但打开开发者工具的Application面板,Cookies里找不到token这个Cookie,Network标签页里响应头虽然能看到Set-Cookie: token=abc123,但它被浏览器忽略了。
原因就是前面说的:跨域请求中,浏览器默认不信任第三方对Cookie的写入。服务端就算下发Set-Cookie,前端只要没有声明要“接管”这个Cookie,浏览器就把它丢掉了。
3.2 完整配置与链路验证
接下来把配置补齐。
后端响应头改成:
javascript复制res.setHeader('Access-Control-Allow-Origin', 'http://localhost:8080')
res.setHeader('Access-Control-Allow-Credentials', 'true')
res.setHeader('Set-Cookie', 'token=abc123; Path=/; HttpOnly')
前端fetch加上:
javascript复制fetch('http://localhost:3000/api/login', {
credentials: 'include'
})
刷新页面再试,Application面板中的Cookies里出现了token这个Cookie,Domain显示为localhost,表示它已经被浏览器接受并存下来了。
下一步验证“携带”功能。再加一个获取用户信息的接口:
javascript复制const server = http.createServer((req, res) => {
if (req.url === '/api/login') {
// 登录接口,下发Cookie
res.setHeader('Access-Control-Allow-Origin', 'http://localhost:8080')
res.setHeader('Access-Control-Allow-Credentials', 'true')
res.setHeader('Set-Cookie', 'token=abc123; Path=/; HttpOnly')
} else if (req.url === '/api/user') {
// 获取用户信息,读取Cookie
res.setHeader('Access-Control-Allow-Origin', 'http://localhost:8080')
res.setHeader('Access-Control-Allow-Credentials', 'true')
const cookie = req.headers.cookie || ''
const hasToken = cookie.includes('token=abc123')
res.writeHead(200, { 'Content-Type': 'application/json' })
res.end(JSON.stringify({ code: 0, user: hasToken ? '张三' : null }))
}
})
前端连续调用两个接口:
javascript复制async function main() {
const loginRes = await fetch('http://localhost:3000/api/login', {
credentials: 'include'
})
await loginRes.json()
const userRes = await fetch('http://localhost:3000/api/user', {
credentials: 'include'
})
const userData = await userRes.json()
console.log(userData)
}
这时后端能收到Cookie,接口返回正常。整个链路就是:登录接口下发Cookie → 前端用credentials: 'include'存储 → 后续请求携带Cookie → 后端读取Cookie完成鉴权。
注意代码里有一处细节:两个接口都必须设置Access-Control-Allow-Origin和Access-Control-Allow-Credentials响应头。很多初学者只给登录接口配了CORS,业务接口忘了配,结果登录成功了,后面的业务请求照样在跨域环节被拦截。
3.3 开发环境代理:另一种省心方案
既然跨域携带Cookie这么麻烦,有没有更省心的方案?有,开发环境代理。
以Vite为例,常见配置如下:
javascript复制// vite.config.js
export default {
server: {
proxy: {
'/api': {
target: 'http://localhost:3000',
changeOrigin: true
}
}
}
}
前端开发时请求/api/login,Vite会把请求转发到http://localhost:3000/api/login。从浏览器的视角来看,请求是同源的,没有跨域问题,Cookie的携带规则回归简单的第一方场景。
但代理方案有一个隐藏坑:代理解决了浏览器的跨域视角,但后端Set-Cookie回来时,Cookie的Domain还是后端域名的。如果后端设置的是Domain=localhost,那前端页面域名也是localhost,端口虽不同但Cookie与端口无关,依然能写入成功。但如果后端设置了Domain=.example.com,那前端开发机的localhost就存不了这个Cookie。这种情况需要在代理层做Cookie改写,或者后端单独给开发环境签发不同Domain的Cookie。
生产环境一般不建议依赖代理,前后端域名不同就是不同域,还是老老实实走CORS加credentials配置。
4. 常见问题排查与避坑
4.1 问题排查顺序
跨域Cookie问题遇到多了,我总结了一套相对高效的排查顺序,建议你按这个顺序来,别一上来就改代码:
- 打开浏览器的Network面板,看请求有没有发出去。
- 如果请求发了但响应被拦截,看Console里的CORS报错具体提示什么。
- 如果请求发出去了,响应也正常,看Response Headers里有没有
Access-Control-Allow-Credentials,值是不是true。 - 看
Access-Control-Allow-Origin是不是具体域名,而不是*。 - 看前端请求有没有设置
withCredentials或credentials: 'include'。 - 如果上面都对,但Cookie仍然没带,看请求里的Cookie字段,以及Cookie的Domain、Path、SameSite属性。
- 如果是登录后跳转再访问接口,还要检查跳转时是否发生了跨站导航,SameSite=Lax是否影响了Cookie发送。
这个顺序能把90%的问题定位出来。
4.2 高频问题速查表
| 现象 | 原因 | 解决方案 |
|---|---|---|
| 跨域请求报CORS错误 | 浏览器拦截了响应,因为后端没配Access-Control-Allow-Origin或值不匹配 |
配置具体源(协议+域名+端口),确认无通配符 |
Network能看到Set-Cookie但Application里没有 |
跨域响应,前端未开启credentials | fetch加credentials: 'include',XHR加withCredentials = true |
| 请求发出但响应被拦截,预检失败 | 自定义请求头未在Access-Control-Allow-Headers中声明 |
后端加上对应的Header字段,比如Content-Type, Authorization |
| 登录成功后,后续业务接口参数正常但无登录态 | 业务接口响应头漏配CORS或credentials | 所有需要鉴权的接口统一配置CORS响应头 |
| iframe跨域嵌入的子应用无法保持登录态 | SameSite=Lax限制iframe第三方上下文携带Cookie | 子应用Cookie设置SameSite=None; Secure,父页面配合HTTPS |
| 本地代理后Cookie写入了但刷新丢失 | 后端Set-Cookie的Domain与前端页面域名不匹配 | 调整Domain属性,或用代理改写响应头 |
| 移动端WebView页面Cookie失效 | WebView默认策略或SameSite限制 | 在WebView设置中允许跨域Cookie,配置Cookie管理策略 |
4.3 容易被忽略的细节
除了上面的高频问题,还有几个细节值得特别提一下。
第一,预检请求(OPTIONS)本身不会携带Cookie。就算你前端配了credentials: 'include',OPTIONS请求也不会带Cookie。有些后端在鉴权中间件里对所有请求都校验Cookie,结果预检请求401,前端照样跨域失败。正确做法是让预检请求直接返回成功,不要走鉴权逻辑。
第二,Access-Control-Max-Age可以指定预检结果的有效期,单位是秒。设置成86400可以有效减少预检请求次数,减轻服务端压力和网络耗时。但要意识到,预检结果缓存是按请求方法+请求头组合来缓存的,只要加了新的自定义头,还是会触发新的预检。
第三,跨域响应里如果业务数据依赖某个响应头(比如自定义的分页字段),需要在后端明确声明Access-Control-Expose-Headers,否则前端JS读取不到对应的响应头。
第四,SameSite=None必须配合Secure属性,而Secure要求HTTPS。本地开发如果只在HTTP下调试,会遇到Cookie下发不了的问题。这在本地联调时很崩溃——生产环境HTTPS一切正常,本地HTTP环境登录态就是起不来。我个人建议本地开发时先临时去掉SameSite=None; Secure,或者本地也起一个HTTPS代理。
5. 跨域场景下的Cookie方案架构思考
5.1 Cookie鉴权与Token方案的取舍
把Cookie的跨域限制搞清楚之后,自然会有一个问题接踵而来:前后端分离架构下,到底还应不应该用Cookie做鉴权?
我的观点是,Cookie做鉴权依然有自己的优势:自动携带、服务端可控、天然支持过期和续期。但跨域场景下它需要你严格配好CORS和SameSite,运维成本和心智负担都不小。
Token方案(比如把Token存LocalStorage或内存中,通过Authorization头携带)从侧面绕开了Cookie的跨域限制,但你照样要面对CORS预检里的Access-Control-Allow-Headers配置。而且LocalStorage存在XSS漏洞下被窃取的风险,Cookie配合HttpOnly反而更安全一些。
所以没有一个绝对好用的方案,关键是你的项目属于哪种场景。同一主域名下的多个子域互相调用,Cookie方案完全够用;如果是完全不相干的域名之间做第三方登录、开放平台授权这类场景,走标准的OAuth授权码流程更正规,也会减少很多Cookie方案带来的兼容性问题。
5.2 单点登录场景里的SameSite应对
多系统单点登录是跨域Cookie问题最集中的地方。几个子域名下的系统各自有Cookie,但要共享登录态,通常会跳转到统一的认证中心去登录。这里有一个细节:从认证中心登录成功后,跳转回业务系统的导航请求是顶级导航,Lax策略允许携带认证中心下发的Cookie。但如果业务系统希望通过XHR/fetch去认证中心校验登录态,XHR请求不会携带Cookie,Lax策略在这里就是最大的坎。
应对方案基本有三条路:
一是把Cookie的SameSite设为None并配合Secure,前提是全站HTTPS。生产环境这是最彻底的解法。
二是把登录态校验改为通过URL跳转参数完成,认证中心登录后携带一次性票据(ticket)跳回业务系统,业务系统再拿票据去换登录态。这种方式规避了跨域Cookie携带问题,但要多一次跳转,流程上需要设计好。
三是同主域名下统一Cookie域名,把所有系统的Cookie都设在同一个顶级域下,通过Path区分业务范围。这种方案在子域间切时不算跨站,Cookie能自然携带。
5.3 生产环境落地建议
一般我落地一套跨域Cookie方案,会按下面的步骤走:
先确认生产环境的协议、域名、端口布局。能上HTTPS就全上,这是SameSite=None的前提。然后统一梳理后端接口,所有需要携带Cookie的跨域接口在网关层统一加上CORS响应头,包括Access-Control-Allow-Origin(具体源)、Access-Control-Allow-Credentials: true、Access-Control-Allow-Headers和Access-Control-Allow-Methods,再配置Access-Control-Max-Age减少预检。
前端在统一请求封装里声明credentials: 'include',不要再每个请求单独写。同时把通配的请求头、Content-Type检查一遍,确保在Allow-Headers里都覆盖。
本地开发优先用代理方案,避免本地环境被HTTPS和SameSite问题干扰。如果确实需要跨域联调,可以用浏览器的--ignore-certificate-errors配合本地HTTPS证书调试,或者临时让后端去掉SameSite,但上线前一定要加回来。
这套组合拳打下来,绝大多数跨域Cookie问题都能解决。剩下的就是耐心排查,把现象和原因对上号,改成正确的配置。跨域问题的坑虽然多,但只要把同源策略、Cookie携带规则、CORS响应头三个环节想通,它就不再是玄学,而是有规律可循的工程问题。
