一次 withCredentials: true 引发的 CORS 跨域问题排查
前言
最近在开发环境中遇到一个比较典型、但排查过程有点绕的 CORS 跨域问题。
现象是:同一份前端代码、同一个接口、同样配置了 withCredentials: true,我这边请求被 Chrome 拦截,而同事电脑上却可以正常请求。
一开始很容易怀疑是浏览器差异、请求头差异,或者服务端 CORS 配置问题。但最终定位下来,真正原因是:同一个接口域名在不同网络环境下命中了不同服务入口,不同入口的 CORS 配置不一致。
这篇文章会从这次真实问题出发,梳理:
withCredentials: true是什么- 它和 CORS 跨域有什么关系
- 为什么
Access-Control-Allow-Origin: *和Access-Control-Allow-Credentials: true不能同时使用 - OPTIONS 预检请求什么时候会出现
- 为什么在 Chrome DevTools 里有时候看不到 OPTIONS
- 如何通过
Remote Address、响应头和网络环境定位问题
一、问题背景
开发环境中,前端项目运行在:
http://localhost:5173
接口请求地址为:
https://api-server-stage.mofaxiao.com/challenge/h5/sendSms
前端使用 axios 发起请求,并配置了 withCredentials: true:
const service = axios.create({
baseURL: import.meta.env.VITE_BASE_URL,
withCredentials: true,
})
在我的 Chrome 浏览器中,请求被 CORS 拦截,控制台出现类似错误:
PreflightWildcardOriginNotAllowed

但是同事使用同一份代码、同一个接口,在他的电脑上可以正常请求。
这就带来了几个疑问:
withCredentials: true到底做了什么?- 为什么
Access-Control-Allow-Origin: *会有问题? - 这个请求到底有没有真正发到服务端?
- 为什么同一份代码,我被拦截,同事却正常?
- 为什么同样是 POST 请求,有时能看到 OPTIONS,有时看不到?
二、withCredentials: true 是什么?
withCredentials 是 XMLHttpRequest 中的配置项,axios 也沿用了这个配置。它的作用是:控制跨域请求时,是否携带用户凭证。
这里的“用户凭证”通常包括:
- Cookie
- HTTP Authentication 认证信息
- TLS 客户端证书
在普通前端业务里,最常见的就是:跨域请求时是否携带 Cookie。
2.1 默认情况下,跨域请求不会携带 Cookie
假设前端页面在:https://www.a.com,接口在 https://api.b.com 这就是跨域请求。
如果你直接发请求:
axios.get('https://api.b.com/user')
浏览器默认不会把 api.b.com 下面的 Cookie 带上。
如果接口依赖 Cookie 判断登录态,那么后端就会认为你没有登录。
2.2 使用 withCredentials 携带 Cookie
2.2.1 axios 中的写法
axios.get('https://api.b.com/user', {
withCredentials: true
})
也可以全局配置:
axios.defaults.withCredentials = true
配置之后,浏览器在发起跨域请求时,会尝试携带目标域名下的 Cookie。
2.2.2 XMLHttpRequest 写法
const xhr = new XMLHttpRequest()
xhr.open('GET', 'https://api.b.com/user')
xhr.withCredentials = true
xhr.send()
2.2.3 fetch 中的对应写法
fetch 中没有 withCredentials,对应配置是 credentials。
fetch('https://api.b.com/user', {
credentials: 'include',
})
常见值:
fetch(url, {
credentials: 'omit' // 不携带 Cookie
})
fetch(url, {
credentials: 'same-origin' // 默认值,同源请求携带,跨域不携带
})
fetch(url, {
credentials: 'include' // 同源和跨域都携带
})
可以简单理解为:
axios withCredentials: true
≈
fetch credentials: 'include'
2.3 只设置前端还不够,后端也要配 CORS
跨域携带 Cookie 时,后端必须返回正确的 CORS 响应头。后端需要配置:
Access-Control-Allow-Credentials: true
Access-Control-Allow-Origin: https://www.a.com
注意: 这时 Access-Control-Allow-Origin 不能是 *。因为浏览器会拦截。
2.4 Cookie 本身也要允许跨站发送
即使前端设置了 withCredentials: true,后端也设置了 CORS,如果 Cookie 属性不对,浏览器仍然可能不带 Cookie。
跨站 Cookie 通常需要:
Set-Cookie: sessionid=1234567890; SameSite=None; Secure
含义是:
SameSite=None 允许跨站发送 Cookie
Secure 只能在 HTTPS 下发送
如果 SameSite 是 Lax 或 Strict,浏览器就不会发送跨站 Cookie。
三、withCredentials 和 CORS 的关系
3.1 withCredentials: true 不等于“允许跨域”
这是一个非常容易误解的点。
withCredentials: true 只是告诉浏览器:
这次跨域请求,我希望携带 Cookie 等用户凭证。
但它并不能决定跨域请求是否能成功。
真正决定跨域请求能不能被浏览器放行的是服务端返回的 CORS 响应头。
3.2 携带 credentials 时,服务端不能返回 *
如果前端请求配置了:
withCredentials: true
服务端不能返回下面这种 CORS 响应头:
Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true
这是一个非法组合。
正确写法应该是返回明确的 Origin(这里是本地开发环境,Origin 就是 http://localhost:5173):
Access-Control-Allow-Origin: http://localhost:5173
Access-Control-Allow-Credentials: true
也就是说:携带 credentials 的跨域请求,Access-Control-Allow-Origin 不能是 *,必须是具体的 Origin。
3.3 为什么浏览器要限制 * + credentials?
如果浏览器允许下面这种配置:
Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true
就相当于服务端允许任意网站带着用户 Cookie 去请求接口。
这会带来非常大的安全风险。例如,用户已经登录了某个业务系统,如果任意第三方网站都能携带用户 Cookie 请求这个系统的接口,那么就可能造成跨站数据泄露。
所以浏览器会强制限制:
当请求携带 credentials 时,服务端必须明确声明允许哪个 Origin。
不能用通配符 *。
四、为什么会出现 OPTIONS 预检请求?
CORS 请求分为两类:
- 简单请求
- 非简单请求
如果是简单请求,浏览器会直接发送真实请求。
如果是非简单请求,浏览器会先发送一个 OPTIONS 预检请求,确认服务端是否允许这次跨域请求。
4.1 withCredentials 本身不会触发 OPTIONS
一个容易误解的点是:
withCredentials: true 本身不会导致浏览器发送 OPTIONS 预检请求。
是否触发预检,主要取决于请求是否属于 CORS 简单请求。
4.2 什么情况下会触发 OPTIONS?
常见触发条件包括:
- 请求方法不是
GET、HEAD、POST - POST 请求的
Content-Type不是简单类型 - 请求带了自定义 Header
简单请求允许的 Content-Type 只有下面几类:
application/x-www-form-urlencoded
multipart/form-data
text/plain
如果请求头是:
Content-Type: application/json
那么这个 POST 请求就是非简单请求,会触发 OPTIONS 预检。
另外,如果请求带了下面这些自定义请求头,也会触发预检:
Authorization: Bearer xxx
X-Token: xxx
X-Requested-With: XMLHttpRequest
X-Brizoo-Token: xxx
X-Brizoo-Request: xxx
4.3 本次问题为什么会触发预检?
本次请求是 POST,并且请求头中有:
Content-Type: application/json
所以浏览器会先发送 OPTIONS 预检请求。
预检请求大致如下:
OPTIONS /challenge/h5/sendSms
Origin: http://localhost:5173
Access-Control-Request-Method: POST
Access-Control-Request-Headers: content-type
服务端需要对这个 OPTIONS 请求返回正确的 CORS 响应头,浏览器才会继续发送真实的 POST 请求。
五、为什么同样是 POST,有时能看到 OPTIONS,有时看不到?
这个问题在本次排查中非常关键。
一开始我在 Chrome DevTools 的 Network 面板里只看到了一个失败的 POST 请求,没有看到 OPTIONS,于是误以为浏览器没有发起预检请求。

后来才发现:请求确实触发了 OPTIONS,只是 DevTools 的筛选条件导致一开始没有看到。
5.1 POST 不一定都会触发 OPTIONS
POST 是否触发 OPTIONS,不取决于“是不是 POST”,而取决于它是否属于 CORS 简单请求。
下面这种 POST 通常不会触发 OPTIONS:
Content-Type: application/x-www-form-urlencoded
或者:
Content-Type: multipart/form-data
或者:
Content-Type: text/plain
并且不能带自定义请求头。
下面这种 POST 通常会触发 OPTIONS:
Content-Type: application/json
或者带有:
Authorization
X-Token
X-Requested-With
X-Brizoo-Token
X-Brizoo-Request
所以,同样是 POST,请求头不同,表现可能不同。
5.2 withCredentials 不会直接触发 OPTIONS
即使设置了:
withCredentials: true
也不代表一定会发 OPTIONS。
它只影响:
跨域请求是否携带 Cookie 等凭证。
真正决定是否发 OPTIONS 的是:
请求方法、Content-Type、是否存在自定义请求头。
本次问题中,触发预检的直接原因是:
Content-Type: application/json
5.3 为什么触发了预检,但 Network 里一开始只看到 POST?
本次排查中还有一个容易误导人的点:
请求确实触发了 OPTIONS 预检;
但是 Chrome DevTools 中如果只筛选 Fetch/XHR,看不到 OPTIONS;
需要切换到 All,才能看到 OPTIONS 预检请求。
也就是说,在 Network 面板中:
Fetch/XHR 视图:只看到失败的 POST
All 视图:才能看到真正的 OPTIONS 预检请求
本次一开始看到的是:
POST 请求标红
Status 显示 CORS error
Size 为 0.0 kB
Headers 里提示 Provisional headers are shown
这很容易让人误以为:
浏览器只发了 POST,没有发 OPTIONS。
但切换到 All 后,才找到了真正的 OPTIONS 请求。
5.4 Provisional headers are shown 说明什么?
当 POST 请求详情里出现:
Provisional headers are shown
通常说明这条真实业务请求还没有完整发出去,Chrome 只是展示了“原本准备发送的请求头”。
结合 CORS 预检失败的场景,可以理解为:
浏览器准备发送 POST;
由于 POST 是非简单请求,先发送 OPTIONS 预检;
OPTIONS 预检没有通过;
真实 POST 被浏览器拦截,没有继续发送。
所以,Network 里看到 POST 标红,并不代表 POST 已经真正到达业务服务。
5.5 如何确认 OPTIONS 是否存在?
可以按下面方式排查:
- 打开 Chrome DevTools → Network
- 勾选
Preserve log - 勾选
Disable cache - 切换到
All,不要只看Fetch/XHR - 右键请求列表表头,勾选
Method、Status、Remote Address - 重新发起请求
- 查看是否存在
Method为OPTIONS的请求
也可以直接在过滤框里输入:
method:OPTIONS
如果在 Fetch/XHR 下看不到,就切到 All 再查。
六、请求到底有没有到服务端?
这个问题要分情况看。
6.1 简单请求
如果是简单请求,浏览器通常会直接发送真实请求。
如果服务端返回的 CORS 响应头不合法,表现会是:
请求已经到达服务端;
服务端也返回了响应;
但是浏览器不允许前端 JS 读取这个响应。
也就是说,服务端可能已经处理了请求,但前端拿不到结果。
6.2 非简单请求
如果是非简单请求,浏览器会先发送 OPTIONS 预检请求。
如果预检失败,表现会是:
OPTIONS 可能已经到达服务端或网关;
但是真实 POST 请求不会继续发送。
本次问题就是这种情况。
由于 OPTIONS 响应头中返回了非法组合:
Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true
浏览器在预检阶段直接拦截,真实 POST 请求没有继续发出。
七、为什么同事可以正常请求?
经过对比,我和同事的关键差异不在前端代码,而在请求链路。
7.1 表面差异:OPTIONS 响应头不同
我这边 OPTIONS 响应头是:
Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true
所以浏览器报错:
PreflightWildcardOriginNotAllowed
同事那边 OPTIONS 响应头是:
Access-Control-Allow-Origin: http://localhost:5173
Access-Control-Allow-Credentials: true
所以他的预检请求可以通过,后续 POST 可以继续发送。
7.2 关键差异:Remote Address 不一样
进一步对比 Chrome DevTools 中的 Remote Address,发现:
我这边:59.110.244.186
同事那边:30.19.0.4
这说明虽然我们访问的是同一个域名:
api-server-stage.mofaxiao.com
但两个人实际命中的服务入口不一样。
7.3 根因:网络环境不同
同事连接了集团的未来盾并连上了内网,因此这个域名被解析或路由到了内网 IP:30.19.0.4
而我没有连接集团内网软件,请求命中了公网入口:59.110.244.186
两个入口的 CORS 配置不一致:
内网入口:返回具体 Origin,配置正确
公网入口:返回 * + credentials,配置错误
这就是为什么同一份代码、同一个接口、同一个浏览器环境,表现却不一样。
八、排查过程总结
这次问题的排查路径可以总结为下面几步。
8.1 第一步:看 Console 报错
Chrome 控制台中的错误信息是:
PreflightWildcardOriginNotAllowed
这个错误基本可以判断:
预检请求返回了 Access-Control-Allow-Origin: *;
同时当前请求又是 credentials 模式;
所以浏览器拒绝继续发送真实请求。
8.2 第二步:看 Network 中的 Method
在 Chrome DevTools 的 Network 面板中,右键请求列表表头,勾选:
Method
Status
Remote Address
重点区分:
OPTIONS:预检请求
POST:真实业务请求
本次排查中还要注意:
不要只停留在 Fetch/XHR 筛选条件下;
需要切换到 All,才能更稳定地看到 OPTIONS 请求。
8.3 第三步:查看 OPTIONS 响应头
重点查看下面几个响应头:
Access-Control-Allow-Origin
Access-Control-Allow-Credentials
Access-Control-Allow-Headers
Access-Control-Allow-Methods
失败请求中,OPTIONS 返回:
Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true
这就是直接原因。
8.4 第四步:对比同事电脑上的响应头
同事电脑上,OPTIONS 返回:
Access-Control-Allow-Origin: http://localhost:5173
Access-Control-Allow-Credentials: true
说明服务端某个入口的 CORS 配置是正确的。
8.5 第五步:对比 Remote Address
最终发现,我和同事命中的远端地址不同:
我:59.110.244.186
同事:30.19.0.4
这说明问题不是前端代码差异,而是网络入口差异。
8.6 第六步:确认网络环境
同事连接了集团内网软件,我没有连接。
因此同一个域名在不同网络环境下命中了不同 IP / 网关,导致 CORS 配置表现不一致。
九、正确的服务端配置方式
9.1 不要使用 * + credentials
错误配置:
Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true
只要前端请求是 credentials 模式,这个组合就会被浏览器拦截。
9.2 使用 Origin 白名单
服务端应该根据请求头中的 Origin 做白名单判断。
例如请求头是:
Origin: http://localhost:5173
如果该 Origin 在白名单中,服务端应该返回:
Access-Control-Allow-Origin: http://localhost:5173
Access-Control-Allow-Credentials: true
Vary: Origin
这里建议加上:
Vary: Origin
因为如果服务端根据不同 Origin 动态返回不同的 Access-Control-Allow-Origin,那么加上 Vary: Origin 可以避免缓存层错误复用 CORS 响应头。
9.3 OPTIONS 也要正确配置
不仅真实业务接口要配置 CORS,OPTIONS 预检请求也要配置正确。
正确示例:
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: http://localhost:5173
Access-Control-Allow-Credentials: true
Access-Control-Allow-Methods: GET, POST, OPTIONS, DELETE, PUT
Access-Control-Allow-Headers: Content-Type, X-Token, Authorization, X-Brizoo-Token, X-Brizoo-Request
Vary: Origin
错误示例:
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true
9.4 多入口要保持配置一致
本次问题的根因之一就是:
内网入口和公网入口的 CORS 配置不一致。
所以如果一个域名在不同网络环境下可能命中不同入口,例如:
- 内网网关
- 公网网关
- 测试环境网关
- 灰度入口
- CDN / WAF
- 本地代理
那么这些入口的 CORS 配置需要保持一致,尤其是 OPTIONS 预检请求的处理逻辑。
十、开发环境中的排查建议
10.1 确认网络环境一致
如果团队使用内网软件、VPN、代理或特殊网关,排查 CORS 问题时一定要确认大家的网络环境是否一致。
重点对比:
Remote Address
DNS 解析结果
hosts 配置
代理设置
VPN / 内网软件状态
10.2 使用命令验证域名解析
可以使用:
nslookup api-server-stage.xxx.com
或者:
dig api-server-stage.xxx.com
如果两个人解析结果不同,那么即使访问的是同一个域名,也可能命中不同网关。
10.3 检查 hosts
在 macOS / Linux 中可以检查:
cat /etc/hosts | grep xxx.com
如果本地 hosts 配置不一致,也可能导致命中的服务入口不同。
10.4 检查代理
如果 DevTools 中看到 Remote Address 是类似下面这种:
127.0.0.1:1082
说明请求可能走了本机代理。
这时需要检查:
- 系统代理
- Chrome 代理插件
- VPN 软件
- Clash / V2Ray / Charles / Whistle / Proxyman / Fiddler 等代理工具
10.5 使用 curl 验证 OPTIONS 响应
可以用 curl 主动模拟预检请求:
curl -i -X OPTIONS 'https://api-server-stage.xxx.com/challenge/h5/sendSms' \
-H 'Origin: http://localhost:5173' \
-H 'Access-Control-Request-Method: POST' \
-H 'Access-Control-Request-Headers: content-type'
重点看:
Access-Control-Allow-Origin
Access-Control-Allow-Credentials
Access-Control-Allow-Headers
Access-Control-Allow-Methods
10.6 对比是否连接内网软件
如果公司内部域名或测试环境依赖内网软件访问,那么需要确认:
是否连接内网软件
连接前后 DNS 解析是否变化
连接前后 Remote Address 是否变化
连接前后 OPTIONS 响应头是否变化
本次问题中,连接集团内网软件后,域名会命中内网 IP,CORS 配置就是正确的。
十一、这次问题的最终结论
本次问题不是前端代码问题,也不是 Chrome 和同事浏览器行为不一致。
真正原因是:
同一个域名在不同网络环境下命中了不同服务入口;
不同入口的 CORS 配置不一致;
我命中的公网入口返回了非法的 * + credentials;
同事命中的内网入口返回了正确的具体 Origin。
具体表现是:
我:Remote Address = 59.110.244.186
同事:Remote Address = 30.19.0.4
我这边 OPTIONS 响应头:
Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true
同事那边 OPTIONS 响应头:
Access-Control-Allow-Origin: http://localhost:5173
Access-Control-Allow-Credentials: true
因此,我这边被浏览器拦截,同事那边可以正常请求。
十二、经验总结
这次问题可以总结为几条经验。
12.1 withCredentials: true 的核心作用
withCredentials: true 用于跨域请求时携带 Cookie 等用户凭证。
它本身不代表允许跨域,也不会直接触发 OPTIONS。
12.2 携带 credentials 时不能使用 *
如果请求携带 credentials,服务端不能返回:
Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true
必须返回具体 Origin:
Access-Control-Allow-Origin: http://localhost:5173
Access-Control-Allow-Credentials: true
12.3 application/json 的 POST 通常会触发预检
POST 请求如果使用:
Content-Type: application/json
通常会触发 OPTIONS 预检。
12.4 预检失败时,真实请求不会继续发送
如果 OPTIONS 预检失败,真实 POST 请求通常不会继续发送。
Network 中看到 POST 标红,不代表 POST 已经真正到达业务服务。
12.5 DevTools 要切换到 All 才更容易看到 OPTIONS
本次问题中,在 Network 面板只筛选 Fetch/XHR 时,没有看到 OPTIONS。
切换到 All 后,才找到了 OPTIONS 预检请求。
所以排查 CORS 时建议:
Network → All → 勾选 Method / Status / Remote Address
12.6 同一份代码表现不同,要重点看 Remote Address
如果你和同事同一份代码、同一个接口,但表现不同,除了看请求头和响应头,还要重点看:
Remote Address
DNS 解析
hosts
代理
VPN / 内网软件
本次问题最终就是通过 Remote Address 定位到的。
附录:CORS 问题排查清单
遇到类似问题时,可以按下面清单排查。
前端侧
- 是否设置了
withCredentials: true? - 是否跨域?
- 请求方法是什么?
Content-Type是什么?- 是否有自定义请求头?
- 是否触发了 OPTIONS?
- Network 是否切到了
All? - 是否勾选了
Method、Status、Remote Address?
服务端 / 网关侧
- OPTIONS 是否返回了 CORS 响应头?
Access-Control-Allow-Origin是具体 Origin 还是*?- 是否返回了
Access-Control-Allow-Credentials: true? - 是否出现了
* + credentials的非法组合? Access-Control-Allow-Headers是否包含实际请求头?Access-Control-Allow-Methods是否包含实际请求方法?- 是否加了
Vary: Origin? - POST 和 OPTIONS 的 CORS 配置是否一致?
网络环境侧
- 自己和同事的
Remote Address是否一致? - DNS 解析结果是否一致?
- hosts 是否一致?
- 是否连接 VPN / 内网软件?
- 是否使用系统代理或浏览器代理插件?
- 是否命中了不同网关、不同节点或不同环境?
结语
CORS 问题表面看是“浏览器拦截”,但背后可能涉及前端请求配置、浏览器安全策略、服务端响应头、OPTIONS 预检、网关配置、DNS、代理和内网环境。
这次问题最大的启发是:
当同一份代码在不同机器上表现不一致时,不要只盯着代码;
一定要对比真实请求链路,尤其是 Remote Address 和 OPTIONS 响应头。
很多时候,真正的差异并不在代码里,而在请求最终命中的那一层入口上。