news 2026/10/20 19:13:02

跨域请求与Cookie携带:从同源策略到CORS全链路解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
跨域请求与Cookie携带:从同源策略到CORS全链路解析

跨域请求和Cookie携带,这两个词在前后端联调时几乎天天遇到。但说实话,我见过不少五年以上经验的开发者,对"为什么这个接口通了、那个却报了跨域错误"、"为什么明明带了Cookie,后端却收不到"这类问题,仍然停留在"配置一下就好了"的层面。一旦出问题,排查链路稍微复杂点就容易卡住。

这本身不是多深不可测的原理,但涉及的协议细节比较多,容易记混。这篇文章就把整条链路拆开讲清楚,覆盖从浏览器同源策略到CORS协议流程,从Cookie的SameSite属性到credentials标志位,再到实际排查链路。适合正在被联调问题折磨的前后端工程师,以及想彻底厘清这套机制的学生或刚转行的人。

1. 跨域请求的完整知识链路

1.1 在浏览器里,跨域到底是什么

通常说"跨域",严格意义上是浏览器的安全行为,而不是协议层面的限制。HTTP协议自己并不知道什么叫跨域,它只负责把请求发出去、把响应拿回来。真正决定"能不能读响应"的,是浏览器在拿到响应之后执行的一套同源检查。

同源的定义非常具体:协议、域名、端口三者完全一致,才算同源。

页面地址请求地址是否同源
https://a.com/pagehttps://a.com/api是
https://a.com/pagehttps://api.a.com否,子域不同
http://a.com/pagehttps://a.com/api否,协议不同
https://a.com:8080/pagehttps://a.com:9090/api否,端口不同

只要三者中有一个不一致,就是跨域。这里有一个容易忽略的盲区:很多人以为子域和主域算同源,实际不算。a.com和api.a.com端口都是443、协议都是HTTPS,但域名不相等,依然属于跨域。同理,localhost和127.0.0.1在浏览器看来也不是同一个源,这个细节在本地联调时经常坑人。

跨域请求发出去了吗?发出了。请求其实已经到达服务器,服务器也正常处理了、响应也返回了。但浏览器检查响应头,发现缺少合适的CORS头,于是拒绝把响应交给JS。也就是说,服务器从日志看一切正常,客户端却一直报错,看起来像是请求"消失"了。这个认知是整个跨域问题排查的基础。

同源策略保护的是用户的登录态和数据。如果没有这套策略,你访问了某个恶意页面,它就可以在后台悄悄发起ajax请求,带着你的Cookie去操作你已登录的其他网站。所以跨域限制不是给开发者添堵,而是浏览器为你的账号安全兜底。

1.2 从GET到预检:完整看懂CORS协议流程

CORS(Cross-Origin Resource Sharing)是浏览器与服务器协商"这个跨域请求允不允许"的标准机制。整个协商过程不复杂,但细节不少。

第一类请求叫简单请求,不需要预检,浏览器直接发出,服务器返回CORS相关响应头即可。简单请求必须同时满足两个条件:方法是 GET、HEAD、POST 三者之一;且自定义头仅限于Accept、Content-Type(仅限application/x-www-form-urlencoded、multipart/form-data、text/plain)等极少数安全头。只要有一项不满足,就不再是简单请求。

第二类请求是其他所有情况,浏览器会先发一次OPTIONS 预检请求,问服务器:我打算这样发起请求,你允许吗?服务器通过响应头告知允许的方式,浏览器再决定是否继续发真正的请求。很多人只见OPTIONS不真实业务请求,以为出错了,其实这是正常流程。

预检请求常见的触发场景是:POST 带application/json的请求体、使用自定义头(比如X-Token)、使用了 PUT/DELETE 等方法。这些场景下的OPTIONS请求很常见,不用慌张。

预检响应里,服务器需要返回以下几个头:

  • Access-Control-Allow-Origin:允许的源,比如https://a.com,如果需要支持任意源但无法携带凭证,可以设成*。
  • Access-Control-Allow-Methods:允许的HTTP方法,比如GET, POST, PUT, DELETE。
  • Access-Control-Allow-Headers:允许的自定义请求头,如果请求里带了X-Token,这里必须显式声明。
  • Access-Control-Max-Age:预检结果缓存时间,单位秒。设置成较大的值可以减少OPTIONS请求次数。

如果是简单请求,则只需Access-Control-Allow-Origin这一个关键头。不过,一旦请求涉及Cookie凭证,情况会再复杂一些,这个放到第3章节详细拆解。

2. Cookie携带的限制:SameSite是头号因素

跨域问题解决了,下一个常见的坑是:跨域请求发出去了,服务器也响应了CORS头,但Cookie没带过去,导致登录接口、鉴权接口全部失效。这里的原因跟CORS没关系,问题在Cookie自身的携带策略上。

2.1 SameSite属性决定跨站请求带不带Cookie

Cookie有个属性叫SameSite,它决定了Cookie在"跨站"请求中是否携带。注意这里是跨站,不是跨域。站的判断依据是"可注册域"(eTLD+1),a.com和b.com是跨站;a.com和api.a.com虽然跨域,但属于同一站。这个区别非常关键:跨域不等同于跨站。

SameSite有三个取值:

属性值行为适用场景
Strict完全禁止跨站携带安全性最高,但用户体验受损,从外部站跳转过来Cookie也没了
Lax允许顶级导航携带,禁止iframe和ajax携带默认值,兼顾安全和体验,日常开发中最常见的取值
None允许所有场景携带,但必须配合Secure跨站场景必须显式设置,同时要求HTTPS

这里要着重说明一个变化。早年间浏览器对SameSite的默认行为是"不加限制",而现在主流浏览器已经将默认值改为Lax。这意味着:一个未显式设置SameSite的Cookie,在跨站的POST请求或iframe里默认不会携带。不少老项目升级了浏览器内核后突然出现"Cookie丢失"的现象,大概率就是这个默认值变化引起的。

我有一个实际项目的例子。某内部管理系统,A用户从邮件系统(外部域)点击链接跳转进入系统首页,首页的/me接口立即请求个人信息。Cookie设置的是SameSite=Strict,跳转进来的导航请求中Cookie直接没带,接口返回401,页面白屏。后来把属性改为Lax,导航请求恢复了,但随之而来的诉求是:某个跨站的iframe里要引入这个小工具的登录态,Strict和Lax都不行,必须SameSite=None; Secure,同时后端还要在CORS里配合允许凭证。这些属性不是随便选的,每个取值背后都有明确的业务场景约束。

对于纯内网工具类系统,一个简单的原则是:如果不涉及被嵌入第三方页面,优先用Lax;如果涉及第三方页面iframe或跨域API调用,才考虑None。None意味着Cookie可以被带到任何站,这对安全边界是有挑战的,尤其要小心XSS配合窃取Cookie的组合攻击。

2.2 Domain与Path属性:Cookie归属边界

除了SameSite,还有Domain和Path决定Cookie向哪些地址发送。很多人在设置Cookie时完全不指定Domain,默认情况下Cookie只会绑定当前完整主机名,子域拿不到。

常见的联调场景:admin.a.com登录后种下的Cookie,业务接口在api.a.com,如果没有把Cookie的Domain设置成a.com,那么业务域的请求根本带不上这个Cookie。解决办法就是在Set-Cookie里显式设置Domain=a.com,这样所有*.a.com的请求都会携带。

Path通常默认/即可,但如果一个域下有多个子系统,可以用Path做隔离。不过从安全角度讲,Path并不能作为权限隔离手段,同一个域下页面都可以读到对方的Cookie,真要隔离就拆域,别依赖Path。

3. 凭证信息标志:跨域请求的"身份证"开关

Cookie要随跨域请求发送,光靠后端在Set-Cookie里设好SameSite还不够。浏览器有一条额外的规则:跨域请求默认不携带任何凭证信息,除非客户端明确打开 credentials 开关,且服务端明确允许。两侧缺一个都不行。

3.1 前端的 withCredentials 与 credentials

原生XMLHttpRequest里对应的是withCredentials = true;fetch里是credentials: 'include';axios里同样是withCredentials: true。但这不说只要设了前端就能成功,后端必须配合。

以fetch为例,不带凭证的写法:

fetch('https://api.example.com/user', { method: 'GET' })

带凭证的写法:

fetch('https://api.example.com/user', { method: 'GET', credentials: 'include' })

axios在创建实例时统一配置:

import axios from 'axios'; const request = axios.create({ baseURL: 'https://api.example.com', timeout: 10000, withCredentials: true });

设置了credentials: 'include',浏览器才会在跨域请求的Cookie头里附带上目标域的名下Cookie。如果不设,即使是完全符合CORS规范的请求,Cookie也不会跟着走。这一步看似简单,却常常被漏掉——尤其是用axios的同学,默认值是withCredentials: false,经常要踩过一次坑才会记得。

3.2 服务端 Access-Control-Allow-Credentials 的限制

前端设了credentials,后端还需要返回Access-Control-Allow-Credentials: true,浏览器才会把响应交给JS。而且当允许凭证时,Access-Control-Allow-Origin不能再用*,必须指定具体源。这是CORS规范里容易被人忘记的硬性要求。

用*加credentials: true的组合,在浏览器里直接报错,类似:The value of the 'Access-Control-Allow-Origin' header in the response must not be the wildcard '*' when the request's credentials mode is 'include'。看到这个报错,就知道是哪里的问题。

一个完整的后端响应应该长这样:

Access-Control-Allow-Origin: https://a.com Access-Control-Allow-Credentials: true Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS Access-Control-Allow-Headers: Content-Type, X-Token

这里有个微妙的点:Access-Control-Allow-Origin的值如果是*,它匹配的不仅仅是单个源,而是"任何源都免凭证放行"。但对于需要凭证的请求,浏览器对所有源都拒绝。所以要么精确返回请求方的源,要么返回*但去掉凭证支持,两者互斥。

4. 一个真实的排查过程:某内部系统登录态丢失之谜

理论部分说完了,讲一个我真实参与排查过的案例。某内部管理系统重构后,前后端拆分部署,前端在app.inner.corp,后端接口在api.inner.corp。登录流程改用跨域请求后,用户反馈:登录接口返回成功,但后续所有带Cookie的请求全部401。

4.1 先看现象,避免开局就改配置

接到问题后我没有直接上代码改,而是先打开浏览器开发者工具的Network面板,一条条看请求。

登录请求本身是成功的,200返回、登录凭证正常。紧接着的/user/info请求返回401,但有意思的是,请求详情里的Request Headers下看不到Cookie头。这说明浏览器压根没在请求里带上Cookie,不是后端拒绝的问题。

然后看Set-Cookie头的原始响应内容:

Set-Cookie: sessionid=abc123; HttpOnly; SameSite=Lax; Secure

问题聚焦到这个Cookie身上:有效域是什么?跨域请求到底是被哪条规则拦下的?

4.2 逐步排除,定位真正的原因

先排除CORS问题——/user/info的响应里,Access-Control-Allow-Origin和Access-Control-Allow-Credentials都正常,后端配置不是这次问题的根因。

接着看Cookie的域匹配。登录接口是api.inner.corp返回的Set-Cookie,如果后端种Cookie时没有写Domain,Cookie默认只绑定api.inner.corp。而前端页面在app.inner.corp,请求/user/info时浏览器会根据请求目标域—也就是api.inner.corp—自动带上它的Cookie。理论上这里能匹配上,所以域不是最大嫌疑。

再回来看SameSite。两个域app.inner.corp和api.inner.corp属于同一个可注册域(inner.corp)吗?这取决于注册域的具体规则,但这种内部系统里Cookie的SameSite类型为Lax时,站点的判断规则会导致即使同一站,某些请求也可能受影响。加上这条请求是AJAX的跨域请求,Lax语义下确实会被拦。

但真正拦截的原因是更前置的一步:前端axios实例没开withCredentials。我在Network里切换到登录请求,看到请求头里同样没有Cookie头,并且在Console里没有相关安全性提示。这种情况下,后端Set-Cookie虽已设置,但浏览器不会在下一次跨域请求中自动重用这个Cookie。因为跨域请求中凭证模式默认是 same-origin,只有显式开启include后,浏览器才会把目标域的Cookie放进请求。

于是完整的排查链路浮出水面:

  1. 登录请求返回了Set-Cookie,但由于前端没有开启withCredentials,浏览器在后续跨域请求中默认不携带凭证。
  2. 由于该Cookie还带SameSite=Lax,即使ajax请求目标域名与页面同站,Lax本身不覆盖这类XMLHttpRequest场景,进一步加大了携带限制。
  3. 后端CORS配置正确,但因为请求本身没有携带Cookie,后端得到的请求是匿名请求,自然401。

修复方案也清晰了:前端axios实例配置withCredentials: true,后端将Cookie调整为SameSite=None; Secure(跨域场景下)并把Access-Control-Allow-Origin改成精确的请求源,同时开启Access-Control-Allow-Credentials。

这个案例的核心教训是:CORS、SameSite、credentials 三者是叠加关系,任何一个环节断链都会导致会话失效,而且报错往往很不直观。排查时不应该盯着报错信息猜,而应该按请求 → 响应头 → Cookie头 → 代码配置的顺序逐段检查。

5. 后端配置与网关反代:实操方案选型对比

深入了解底层机制后,落地实践时不同团队适合不同方案。这里把我的做法和适用条件整理一下。

5.1 后端CORS过滤器/拦截器

在Java Spring生态里,如果用全局CORS配置,一个大致的写法(以Spring Boot为例):

@Configuration public class CorsConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/api/**") .allowedOrigins("https://app.inner.corp") .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") .allowedHeaders("Content-Type", "X-Token") .allowCredentials(true) .maxAge(3600); } }

注意allowedOrigins用了精确源而不是*,并且配合allowCredentials(true)。这是跨域携带凭证的必要条件。Node.js服务用Express的话,通常配合cors中间件:

const cors = require('cors'); app.use(cors({ origin: 'https://app.inner.corp', credentials: true, allowedHeaders: ['Content-Type', 'X-Token'], maxAge: 3600 }));

这两种方案的优点是灵活、能按路径维度控制。缺点是每个服务都得配一遍。如果服务数量上来了,配置反复复制会出问题,特别是源或允许头不一致的时候,排查起来格外痛苦。

5.2 网关反代:把跨域变成同源

另一个很常见的方案是让网关/反向代理把所有跨域请求转发,从浏览器视角看是"同源"请求,根本不会触发CORS。配置示例用Nginx:

server { listen 443 ssl; server_name app.inner.corp; location /api/ { proxy_pass https://api.inner.corp/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_pass_request_headers on; } }

在这个方案里,前端只访问app.inner.corp/api/...,浏览器认为这是同源请求,不存在跨域问题。Cookie在app.inner.corp域下正常种下,请求也自然携带。这个方案对Cookie携带的限制最少,是很多小型项目和单体应用的优选。

它的主要问题是需要把API路径规划好,代理规则不能跟业务路径冲突。如果团队内部系统多,网关层负责路由,还是需要一个全局的CORS策略来兜底,因为并不是所有场景都能走代理。

5.3 多环境分隔后的方案调整

当服务拆成多个子域,比如静态资源在cdn.inner.corp,前端在app.inner.corp,SRE和测试环境往往域名也不同,每个环境都得把CORS源维护维对。环境越多越需要把"允许源列表"配置独立出来,按环境注入,而不是写死在代码里。

我常用的做法是做成环境变量:

环境允许源
开发http://localhost:3000, https://dev-app.inner.corp
测试https://test-app.inner.corp
生产https://app.inner.corp

后端启动时读取配置生成CORS响应头。这样避免了提交代码改配置的低效循环,也降低了误把测试源放到生产环境的概率。

6. 跨域安全设计:别把CORS变成攻击面

跨域请求和Cookie携带不只是技术问题,也是一个安全边界问题。很多团队把CORS当成"帮我放行"的工具,配置成*一劳永逸。但这么做等于把接口的读取权限开放给所有网站,任何外部恶意页面发起跨域请求,浏览器都会直接放行响应,代价是数据可被任意第三方页面读取。

安全设计上有几条基础原则值得写进规范。

第一,生产环境严禁使用Access-Control-Allow-Origin: *且开启凭证模式。这两者本来就互斥,如果单独使用*且接口暴露的是非敏感数据,勉强可以;但接口一旦涉及用户数据,必须用精确源。对外提供一个统一的JSON数据服务,且明确不携带凭证的场景,才适合考虑通配符。

第二,Cookie尽量打上Secure和HttpOnly标志。HttpOnly防止JS读取Cookie从而降低XSS后Cookie被直接盗走的可能性;Secure确保Cookie只通过HTTPS传输,避免被明文拦截。现代浏览器同样在推动强制这些标志,新接口的Set-Cookie没有加Secure的话,在HTTPS页面上会出现警告甚至被拒绝。

第三,仍然需要警惕Cookie的CSRF风险。如果Cookie在同站里会被自动携带,攻击者可以诱导用户发起一个同站请求。这时候推荐做法是引入CSRF Token,或者在关键请求里校验自定义头(如X-Requested-With)。CORS帮我们挡住了跨域读取,但没有完全挡住跨域写入,登录态相关的写操作仍需额外防护。

第四,预检请求的结果要合理设置缓存时长。Access-Control-Max-Age建议300到3600秒之间。设太短会让每个请求都多一次OPTIONS,设太长又会让服务端调整CORS策略后,浏览器继续按旧策略执行48小时。改动CORS后想让策略尽快生效,可以把Max-Age先调小,或临时清掉缓存。

对应用架构的启示也在这里体现:能用代理别频繁用CORS,内部系统把静态资源与API放在同一域名下,或者通过网关聚合,降低跨域复杂度,也就减少了安全配置失误的机会;但对外提供的开放API,正确配置CORS反而是更标准、更可控的方式。

7. 还有一条路:JSONP与其他替代方案

在CORS成为主流之前,跨域读取有一个常用手段叫JSONP。它借助<script>标签不受同源策略限制的特性,用动态插入script的方式,让服务端返回一段可执行的JS回调,绕过跨域限制。

一个最简单的JSONP请求过程是这样的:前端构造一个带callback参数的script标签,比如https://api.inner.corp/user?callback=handleResult,后端返回内容为handleResult({name: 'test'}),浏览器加载这段JS后自动执行回调函数,从而拿到数据。

JSONP的优势在于兼容老浏览器,且不需要服务端配置CORS头。但它天然有几个致命限制:只能GET、无法设置自定义安全头、存在脚本注入风险(回调参数必须做严格白名单校验)。现代开发中,除非还在维护IE级老系统,否则没有必要用JSONP。官方API和现代前端框架的默认模式已经完全转向CORS。

另一种方案是iframe +postMessage。有些场景是嵌入了第三方报表系统,报表页和服务页面不同域,但需要通信。这种情况既敲不了AJAX跨域,也不适合简单CORS配置,就考虑通过postMessage包装消息来传递。但这种方案不适合高频API调用,只适合事件通知和小数据交换。

这些替代方案不一定都用得上,但有意识地知道还有这条路径,遇到一些特殊的老项目或第三方受限环境时,不至于完全没思路。

在我经历过的项目里,大原则始终没变:能同源就同源,不能同源就精确配置CORS,凭证模式保持显式声明。把这套理顺,跨域的问题就不再是玄学。如果你正在面对这类困惑,建议先抓包看请求,再逐项核对三个关键头,往往很快就能找到断点所在。其他细节反而是次要的了。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/20 19:12:59

Live2D Cubism Editor 5.3:从建模到导出的实操升级指南

Live2D Cubism Editor 5.3 正式版发布了。这个消息在折腾虚拟形象和二维动画的人群里炸开了一小圈&#xff0c;毕竟很多人从预览版就开始蹲&#xff0c;想看看正式版到底把哪些遗留问题收拾干净了。我也和几位做模型的朋友第一时间装上了新版&#xff0c;把手头几个角色全部过了…

作者头像 李华
网站建设 2026/10/20 19:12:27

性能测试实战:从并发压测到瓶颈定位的排查指南

做性能测试这些年&#xff0c;最常被问的一句话是“系统到底能扛多少并发”。但真到了压测现场&#xff0c;你发现最难的往往不是把TPS顶上去&#xff0c;而是那些此起彼伏的报错、忽高忽低的响应时间、压测机自己先挂掉、测试数据一跑就失效……一堆问题堆在一起&#xff0c;你…

作者头像 李华
网站建设 2026/10/20 19:12:01

基于MATLAB的垃圾多种类分类识别系统GUI开发实践

其实很多朋友问过我&#xff0c;为什么拿 MATLAB 做图像分类识别&#xff0c;而不是直接用 Python。我的回答通常是&#xff1a;如果只是自己练手&#xff0c;随便什么语言都行&#xff0c;但如果是课程设计或者毕业设计&#xff0c;MATLAB 的 GUI 开发效率和信号处理工具箱确实…

作者头像 李华
网站建设 2026/10/20 19:11:41

车载中控UI自动化测试实战:从Appium框架到音频校验与稳定性优化

当你坐在驾驶座上&#xff0c;车机大屏从黑屏到亮起&#xff0c;再到空调面板弹出、导航语音播报、360影像切换&#xff0c;这些界面的每一帧交互背后都是需要验证的。我做了几年的车载中控UI自动化测试&#xff0c;从最开始用Appium点按钮点到怀疑人生&#xff0c;到后来形成一…

作者头像 李华
网站建设 2026/10/20 19:11:38

用 Vault 统一管理 vLLM 部署中的密钥:注入、审计与轮转

部署 vLLM 这半年&#xff0c;最让我头疼的从来不是模型显存不够或者推理速度上不去&#xff0c;而是那些散落各处的 API Key。模型仓库的下载凭证、外部嵌入服务的访问密钥、给调用方签发的服务端鉴权 Key……都在环境变量里、配置文件里、启动脚本里&#xff0c;甚至某次聊天…

作者头像 李华
网站建设 2026/10/20 19:10:47

TensorFlow深度学习入门:从张量到模型部署的工程实践

1. 为什么“tensorflow”这个标题值得单独拎出来聊如果你在技术社区里闲逛&#xff0c;看到有人只甩出一个词——“tensorflow”&#xff0c;然后就没下文了&#xff0c;你大概会跟我一样&#xff0c;第一反应是&#xff1a;这哥们儿要么是在抛一个巨大的话题等别人接&#xff…

作者头像 李华