CORS 跨域报错与预检失败怎么解决:blocked by CORS policy 的原因、排查与配置
前端调接口时,控制台突然出现一串红字:Access to fetch at 某个接口地址 from origin 某个本地地址 has been blocked by CORS policy,后面跟着几种不同的结尾——No 'Access-Control-Allow-Origin' header is present on the requested resource,或者 Response to preflight request doesn't pass access control check,或者 The value of the 'Access-Control-Allow-Origin' header must not be the wildcard when the request's credentials mode is 'include'。网络面板里对应的请求显示 CORS error 或 net::ERR_FAILED。最常见的场景是本地开发调用测试环境接口、前端直接调用对象存储或第三方 API、网页通过 CDN 加载字体与脚本,以及在当前网络下访问境外接口不稳定。
CORS 是浏览器的安全机制:网页只能读取同源接口的响应,跨源时要由服务器在响应头里明确授权。所以 CORS 报错的修复几乎都在服务端或者网关,前端能做的只是不要无谓地触发预检。但有一类情况经常被误判:请求在网络层就失败了,或者被网关、防火墙返回了不带 CORS 头的错误页,浏览器同样会报 CORS 错误。下面先讲清楚简单请求与预检请求的区别,再按步骤排查。
简单请求、预检请求与带凭据请求的区别
把 Content-Type 设为 application/json、加一个 Authorization 头、用 PUT 或 DELETE 方法,都会让请求变成需要预检的请求。浏览器会先发一个 OPTIONS 请求,问服务器允不允许这个来源、这个方法、这些请求头,服务器回答允许,才会发真正的请求。很多后端框架只给业务路由配了 CORS,没有处理 OPTIONS,预检就会返回 404 或 405,这是最常见的预检失败原因。
| 对比项 | 简单请求 | 预检请求 | 带凭据请求 |
|---|---|---|---|
| 触发条件 | GET、HEAD、POST 且只用基本头 | 其他方法、自定义头或 JSON 类型 | fetch 设置 credentials 为 include |
| 是否先发 OPTIONS | 否 | 是 | 取决于是否满足简单请求 |
| 服务端必须返回 | Allow-Origin | Allow-Origin、Allow-Methods、Allow-Headers | 具体的 Origin 与 Allow-Credentials |
| 能否用通配符 | 可以 | 可以 | 不可以 |
| 典型报错 | No Allow-Origin header | Response to preflight 不通过 | wildcard 与 credentials 冲突 |
| 常见场景 | 读取公开数据 | 提交 JSON、带 Authorization 头 | 依赖 Cookie 的登录态 |
按顺序排查:分步操作
- 第一步:先把网络线路准备好。如果接口部署在境外,比如海外云服务器、境外对象存储或第三方 API,先在开发电脑上连上刺猬VPN 再打开目标网站与接口,排除网络层失败被误报成 CORS 的情况;客户端自定义用户名和密码即可匿名注册,不需要邮箱和手机号;
- 第二步:打开浏览器开发者工具的网络面板,勾选保留日志,重新触发请求,找到同一个接口的两条记录:一条类型是 preflight 的 OPTIONS 请求,一条是真正的请求;
- 第三步:看 OPTIONS 请求的状态码。返回 200 或 204 才算正常;返回 404、405 说明服务器没处理 OPTIONS;返回 401、403 说明鉴权或防火墙拦在了预检之前;没有状态码说明请求在网络层就失败了;
- 第四步:看 OPTIONS 响应头。Access-Control-Allow-Origin 要等于页面的来源,或者是星号;Access-Control-Allow-Methods 要包含实际方法;Access-Control-Allow-Headers 要包含你发的自定义头;
- 第五步:用命令行复现预检。执行 curl -i -X OPTIONS,加上 Origin 与 Access-Control-Request-Method 两个请求头,直接看服务器返回了什么,能排除浏览器缓存与扩展的干扰;
- 第六步:在服务端补齐配置。让 OPTIONS 请求在鉴权之前直接返回,响应头写明允许的来源、方法与请求头,并用 Access-Control-Max-Age 缓存预检结果,减少重复的 OPTIONS;
- 第七步:需要带 Cookie 时,前端设置 credentials 为 include,服务端的 Allow-Origin 必须写具体来源而不是星号,同时返回 Access-Control-Allow-Credentials 为 true;
- 第八步:中间有网关、CDN 或对象存储时,在它们那一层也要配置 CORS,或者确认它们会透传源站的 CORS 头。对象存储通常要在存储桶设置里单独添加 CORS 规则;
- 第九步:本地开发阶段可以用开发服务器的代理功能,把接口转发到同源路径,避开跨域;但上线前一定要在正式环境把 CORS 配好,不要依赖代理。
按现象排查:报错原文对应的原因
CORS 报错的后半句信息量最大,先看清楚是哪一种。
- No 'Access-Control-Allow-Origin' header is present:服务器的响应里没有授权头,可能是根本没配,也可能是返回了一个不带头的错误页;
- Response to preflight request doesn't pass access control check:预检失败,看 OPTIONS 的状态码,多半是 404、405 或被鉴权拦截;
- Allow-Origin 不能是通配符:带凭据的请求不允许用星号,改成返回具体来源;
- Request header field 某某 is not allowed:自定义请求头没写进 Allow-Headers,补上即可;
- Method 某某 is not allowed:实际方法没写进 Allow-Methods;
- Allow-Origin 出现了两个值:网关和源站都加了头,导致重复,只保留一处;
- 状态码显示 403 加 CORS 报错:防火墙或权限规则返回了 403,错误页没有 CORS 头,先解决 403 本身;
- 网络面板显示 net::ERR_FAILED 且没有状态码:请求在网络层失败,可能是超时、连接被重置或者 DNS 解析失败,先检查网络;
- 只在某些用户那里报错:这些用户的网络访问接口不稳定,或者经过了会改写响应头的代理。
网络因素:哪些 CORS 报错其实是网络问题
开发者访问境外接口时,网络层失败很常见:连接超时、被重置,浏览器拿不到任何响应,只能报一个笼统的 CORS 错误。这时候改服务端配置毫无作用,先连上线路确认接口能通。免费节点永久免费、不限流量、不限速,适合临时验证;每天都要调试境外接口的开发者,时长套餐有效期内不限流量、不限速,断线保护能避免调试到一半连接改走直连。实际可用性取决于当前网络环境。
| 对比项 | 真正的 CORS 配置问题 | 网络层失败 | 网关或防火墙拦截 |
|---|---|---|---|
| OPTIONS 状态码 | 有,通常 200 但缺头 | 没有状态码 | 403、502、504 等 |
| curl 复现 | 能复现,头缺失 | 命令行同样超时 | 返回错误页 |
| 换网络后 | 仍然报错 | 可能恢复 | 视规则而定 |
| 影响范围 | 所有用户 | 部分网络下的用户 | 触发规则的请求 |
| 修复位置 | 服务端配置 | 网络或线路 | 网关与防火墙规则 |
| 排查工具 | 网络面板与 curl | ping 与 curl | 网关日志 |
常见后端与网关的配置要点
Nginx 上配置 CORS,要用 add_header 加上 always 参数,否则 4xx、5xx 的错误响应不会带上 CORS 头,前端看到的就永远是 CORS 报错而不是真正的错误码;同时用 if 判断请求方法为 OPTIONS 时直接返回 204。Node.js 的 Express 用官方的 cors 中间件,注意要放在所有路由与鉴权中间件之前。Spring、Django、Laravel 等框架都有现成的 CORS 配置项,优先用框架自带的,不要手写多处响应头。
对象存储与 CDN 是另一个高发点:前端直接上传文件到存储桶、从 CDN 加载字体时,存储桶要单独配置允许的来源与方法,CDN 要把 Origin 纳入缓存键,否则会把给 A 网站的响应缓存后发给 B 网站,出现时好时坏的 CORS 报错。改完配置后清一次 CDN 缓存再测试。
命令行对照:用 curl 区分三种情况
浏览器的报错信息是被简化过的,用 curl 能看到服务器的原始响应。执行 curl -i -X OPTIONS 加接口地址,再用 -H 带上 Origin 与 Access-Control-Request-Method 两个请求头,然后看输出。第一种情况:返回 204 或 200,但响应头里没有 Access-Control-Allow-Origin,说明是服务端没配 CORS,回去改配置;第二种情况:返回 403、404 或 405,说明 OPTIONS 被鉴权、路由或防火墙拦住了,要让预检在这些规则之前直接返回;第三种情况:命令长时间没有输出,最后提示连接超时或被重置,说明问题根本不在 CORS,而是网络到不了服务器。
第三种情况最好再做一次对照:先在当前网络执行一次,再连上线路执行一次。线路下能拿到正常响应,就说明是当前网络访问这个接口不稳定,浏览器那边的 CORS 报错只是表象。前端团队成员遇到问题时,把这条 curl 命令与输出贴进问题描述里,后端同事能在一分钟内判断该谁改,比截图控制台红字有用得多。
还有两个小技巧:在 curl 命令里加 -v 能看到 TLS 握手与重定向过程,接口返回 301 跳转到另一个域名时,跳转后的域名同样需要配置 CORS;把真正的请求也用 curl 发一遍,带上相同的请求头与请求体,能确认业务接口本身是否正常,而不是只看预检。排查结束后记得清掉浏览器的预检缓存,最简单的办法是在网络面板勾选停用缓存再刷新,否则旧的预检结果可能让你误以为改动没有生效。
和其他访问错误的分工
很多 CORS 报错的根源是 403:防火墙、访问控制规则或者签名校验失败,返回了一个不带 CORS 头的错误页。遇到网络面板里状态码是 403 的情况,先按 网站 403 Forbidden 修复指南 解决权限问题,CORS 报错往往会一起消失。
前端直接读写对象存储时,Cloudflare R2 的 CORS 规则在存储桶设置里配置,规则格式与 S3 相近,配置与预签名上传的细节见 Cloudflare R2 存储使用指南。分工可以这样记:报错后半句告诉你缺什么头,状态码告诉你是不是别的错误,curl 告诉你是不是网络问题。
常见报错速查表
| 现象 | 原因 | 解决办法 |
|---|---|---|
| No Allow-Origin header | 响应缺少授权头 | 服务端补齐 CORS 头 |
| 预检请求不通过 | OPTIONS 返回 404 或 405 | 让 OPTIONS 在鉴权前返回 204 |
| 通配符与凭据冲突 | 带凭据请求用了星号 | 返回具体来源与 Allow-Credentials |
| 请求头不被允许 | Allow-Headers 缺少该头 | 把自定义头加入允许列表 |
| Allow-Origin 重复 | 网关与源站都加了头 | 只在一处配置 |
| 403 加 CORS 报错 | 防火墙返回无头错误页 | 先解决 403 本身 |
| net::ERR_FAILED 无状态码 | 网络层失败 | 检查网络或连上线路后重试 |
高频问答
- 问:能在前端关掉 CORS 吗?答:不能,这是浏览器的安全机制,只能由服务器授权,浏览器扩展关闭 CORS 只适合本地临时调试;
- 问:为什么 Postman 能请求成功,浏览器却报错?答:CORS 只在浏览器里生效,命令行与调试工具不检查跨域;
- 问:Allow-Origin 写星号安全吗?答:公开数据可以,涉及登录态或敏感数据时应写具体来源;
- 问:怎么减少预检请求?答:能用简单请求就不要加自定义头,必须预检时用 Access-Control-Max-Age 缓存结果;
- 问:为什么只有部分用户报 CORS 错误?答:多半是这些用户的网络访问接口失败,或者经过了改写响应头的代理;
- 问:本地开发代理能代替 CORS 配置吗?答:只能用于开发阶段,上线后浏览器直接访问接口,仍需正确配置。
处理 CORS 报错的正确思路
CORS 跨域与预检失败,看起来是一种错误,实际上有三种来源:服务端没有正确授权,网关或防火墙返回了不带头的错误页,请求在网络层就失败了。排查顺序固定下来就不会乱:先看 OPTIONS 的状态码,再看响应头缺什么,然后用 curl 复现,最后才去改配置;访问境外接口时,先确认网络通了再动代码。
刺猬VPN 采用 HTTPS+AES 加密传输,自定义用户名和密码即可匿名注册,不需要邮箱和手机号;免费节点永久免费、不限流量、不限速,适合临时验证境外接口能否连通;时长套餐有效期内不限流量、不限速,适合每天调试海外服务的开发者。客户端带断线保护与按域名分流,一个账号支持多设备同时在线。实际可用性取决于当前网络环境。
换刺猬VPN告别连接烦恼:一键连接、连接稳定
刺猬VPN提供永久免费节点,不限流量;无需邮箱,自定义用户名即可匿名注册。一个账号支持多设备同时在线,实际可用性取决于当前网络环境。