前端跨域问题怎么解决?从控制台报错到响应头的排查顺序
- 2026年11月10日
- 25 次
手上这套后台跑了三年多:前端打出来的 dist 扔在一台 nginx 上,接口在 api 子域,两套域名从上线起就是分开的。这种结构下,前端跨域问题怎么解决成了每加一个接口都要重新过一遍的事。常见处理方式其实只有四种——本地代理、nginx 反代同源、后端加 CORS 头、JSONP,各自的前提和代价差得很远,选错一次后面全是补丁。
先读控制台里那条 CORS 报错
排查的起点在浏览器,不在服务器。F12 打开 Console,红字第一行就是病因,Chrome 把跨域失败分成了两类,读法完全不一样。

一类是请求没发出去,报错原文长这样:
Access to fetch at 'https://api.example.com/user' from origin 'https://www.example.com' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource.
关键在 preflight 这个词,说明卡在预检,请求根本没进到业务逻辑。
另一类是请求发出去、响应也回来了,浏览器却拒绝把响应交给 JS。Console 只留一句 TypeError: Failed to fetch,Network 里那条请求明明白白是 200。
分辨这两类是全部工作的分水岭,前端跨域问题怎么解决的顺序也是从这里定下来的:先确认请求有没有发出去,再谈改哪一层。
浏览器先做一次同源判定,标准只有一条:协议、域名、端口三者全等。https://a.com 和 https://a.com:8443 不同源,https 页面调 http 接口也不同源——后一种还会先被 mixed content 拦掉,报错长得跟 CORS 无关,方向很容易看偏。
Network 面板里请求到底发出去没有
Console 给结论,过程在 Network。切到 Fetch/XHR 过滤,看那条请求在不在列表里。在,说明浏览器允许发送;不在,说明预检没过,或者被某个扩展拦截在更前面。
预检请求是单独一行,Type 列写 preflight,Method 是 OPTIONS,紧挨着主请求。这两个值认出来,后面的判断才有依据。
有一种状态最有迷惑性:Status 列 200,Size 也有数字,JS 里却只抛 TypeError: Failed to fetch。这通常是响应头写的是 *,而 fetch 带了 credentials: 'include',浏览器收下响应,然后整份丢掉,一句提示都不给。

所以查前端跨域问题怎么解决,第一步落在 Network 而不是配置文件。
预检 OPTIONS 返回 204 还是 403
找到 OPTIONS 那一行,看状态码。204 或 200 表示预检被服务端正常接住了,缺的只是响应头;403、401 是另一回事——网关、WAF 或者鉴权中间件把 OPTIONS 挡在了 CORS 逻辑之前,这时候改 add_header 一点用没有。
这两种情况我在一个挂了 CDN 的站上都遇到过。源站 nginx 的配置是对的,curl 直连源站 OPTIONS 回 204,走域名就是 403,最后查出来是 CDN 的回源规则只放行了 GET 和 POST。
不依赖浏览器也能验,一条 curl 就够:
curl -s -o /dev/null -D - -X OPTIONS 'https://api.example.com/user' \
-H 'Origin: https://www.example.com' \
-H 'Access-Control-Request-Method: POST'
输出是响应头全文,第一行是状态码,往下找 Access-Control-Allow-Origin 那一行。没有它就是没放行,有它但值跟你的 Origin 对不上,照样拦。
还有一个前提经常被漏掉:OPTIONS 本身不能要求鉴权,带 Authorization 的接口如果预检就 401,主请求永远发不出去。
Access-Control-Max-Age 决定预检结果的缓存时长,Chrome 封顶 7200 秒。缓存期内改了头,浏览器不会重新问,看着像没生效,换个路径或者清一次缓存才看得到。
响应头缺少 Access-Control-Allow-Origin
排除到这一步,问题就剩一行头。nginx 里的写法:
add_header Access-Control-Allow-Origin $cors_origin always;
add_header Access-Control-Allow-Credentials true always;
add_header Access-Control-Allow-Headers 'content-type' always;
add_header Access-Control-Allow-Methods 'GET,POST,OPTIONS' always;
if ($request_method = OPTIONS) { return 204; }
always 不能省。add_header 默认只对 200、201、204、206、301、302、303、304、307、308 生效,401 和 500 的响应不带这个头。接口一报错,前端看到的是跨域错误而不是真实报错,能把人带偏很久。
还有一条继承规则更隐蔽:location 里只要出现过任何一条 add_header,http 和 server 块里的 add_header 就整体失效。
我最初以为把 CORS 头写在 http 块里能全站生效,后来发现有个 location 为了加缓存头写了一行 add_header,那个 location 的跨域全废了。改法是在 location 里补一遍,或者用 include 把公共头引进来。
$cors_origin 来自 http 块里的 map,命中白名单才回显请求的 Origin,没命中给空串,nginx 值为空时不输出这一行头。直接回显 $http_origin 而不校验,效果跟写 * 一样,等于谁都能读。

改完先 nginx -t 过语法,再 reload,然后复测一次。
四种做法摆在一起比
本地代理,vite 的 server.proxy 或者 webpack 的 devServer.proxy。前提是只在开发机上,代价是打包上线即失效,适合本地联调。
nginx 反代同源,把 /api 转发到后端。前提是前后端在同一台入口后面,代价是多一层转发、日志要看两处,适合自有后台。
后端直接加 CORS 头。前提是能改后端代码或者能改网关,代价是每个环境都得配一遍、白名单还得维护,适合对外提供的接口。
JSONP 的前提是接口只读、只走 GET,代价是后端要包一层回调、出错拿不到状态码,只适合老系统里已有的接口。
把前提、代价、适用场景三个维度摆在同一张桌上,前端跨域问题怎么解决的答案就不含糊了。我现在是自己后台走反代同源,第三方域名来调的接口走 CORS 白名单——同源没有预检、没有 cookie 的 SameSite 问题,出问题只在一个地方查。
两种放行方式怎么选
白名单那条路上还有个岔口:Access-Control-Allow-Origin 写 *(星号),还是写具体域名。这不是宽松程度的差别,是能不能带 cookie 的差别。
写星号的前提是接口不需要登录态,代价是任何站点都能读你的响应,公开的只读接口无所谓,带用户数据的就不行。
写具体域名要多做两件事:服务端回 Access-Control-Allow-Credentials: true,前端 fetch 带 credentials: 'include',缺一边 cookie 都过不去。
const res = await fetch('https://api.example.com/user', {
method: 'POST',
credentials: 'include',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id: 1 }),
});
两边都写对才算数。星号和 credentials: 'include' 撞在一起,不是降级成不带 cookie,是整个请求失败,Chrome 的原话是 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'.
回到这套结构上
这套结构下前端跨域问题怎么解决,卡住我的从来不是某条配置,是顺序。当时我先改 nginx,改完刷页面还是红字,因为预检那一步就被 401 拦掉了——改的是第三层,故障在第二层。
如果重来,我会先用一条 curl 把 OPTIONS 打到 204,确认预检通了再动响应头。五分钟的事,省掉的是来回折腾一晚上。
代价也很实在:那两天我前后端各加了一条头,响应里出现两个 Access-Control-Allow-Origin,浏览器报的是 multiple values,比原来的错还难读。
修法是只让一层发头。nginx 里加一行 proxy_hide_header Access-Control-Allow-Origin;,Access-Control-Allow-Credentials 那条同理,把上游那份藏掉,头由 nginx 统一发。上游是自己写的服务,就改上游,别两边都留。
若非作者标注皆为李涛自留地原创文章,转载或复制请以超链接形式并注明出处。
- Nginx 504 Gateway Timeout超时详解:原因与优化方案 2026-07-02
- css控制div垂直居中的几种写法与浏览器兼容性对比 2026-11-07
- ZblogPHP目录索引插件不生效问题及解决办法 2022-10-20
- PhpStudy小皮面板提示80端口被系统占用该怎么办呢? 2022-09-30
- 宝塔面板网站502 Bad Gateway报错的N种原因和解决方法 2026-07-02
- ICP备案需要钱么?ICP备案怎么查询? 2022-10-07
