接口用 curl 能返回 JSON,浏览器却提示 blocked by CORS policy?先别换 VPS,也别把所有响应头都改成星号。先在浏览器 Network 面板看失败的是 OPTIONS 预检,还是后面的真实请求,再核对响应头。
本文解决前端和 API 分开域名部署后的跨域问题:例如页面在 https://app.example.com,API 在 https://api.example.com。示例只允许一个明确来源,域名、路径和后端端口都要按实际项目替换。
MDN 的 CORS 说明讲的是浏览器对跨来源脚本读取响应的控制。协议、主机名或端口不同,都可能跨来源;同一个主域名下的不同子域也不例外。
但“curl 成功”只代表这次 HTTP 请求有响应,不代表浏览器会允许页面读取。反过来,Network 面板若显示 502、证书错误、连接失败,也不能只靠加 CORS 头解决。
| 浏览器现象 | 优先检查 | 修复位置 |
|---|---|---|
| OPTIONS 返回 401/403 | 预检被登录验证或 WAF 拦截 | 网关、认证中间件 |
| OPTIONS 返回 301/302 | HTTP/HTTPS、斜杠或登录页跳转 | 请求 URL、反代规则 |
| 提示 Allow-Origin 缺失 | 真正返回响应的那一层没配置 | 应用、Nginx 或 CDN |
| 提示 Allow-Origin 有多个值 | 两层重复加响应头 | 保留一个配置责任方 |
| 带 Cookie 时失败 | 来源是否为星号、凭据配置 | 前端请求与服务端 |
| 只在接口报错时显示 CORS | 错误响应没带跨域头 | 错误处理和响应头规则 |
打开 Network,选择接口请求,记录 Request URL、Origin、状态码和响应头。不要把含 Cookie、Authorization 的完整请求截图公开。服务器端的鉴权和 CSRF 防护仍然需要保留,CORS 不会阻止 curl 直接调用接口。
JSON POST、携带 Authorization 等请求通常会触发预检。按 MDN 预检说明,浏览器先发 OPTIONS,询问目标来源是否允许后续方法和请求头。
在自己的终端模拟一次:
curl -i -X OPTIONS 'https://api.example.com/api/profile' \
-H 'Origin: https://app.example.com' \
-H 'Access-Control-Request-Method: POST' \
-H 'Access-Control-Request-Headers: authorization,content-type'
这里没有真实 Token,也不需要给预检附加 Cookie。检查:
Access-Control-Allow-Origin是否对应前端的完整来源;Access-Control-Allow-Methods是否包含 POST;Access-Control-Allow-Headers是否包含实际请求要使用的头。
预检返回 204 很常见,但状态成功还不够,头也要匹配。不要用提高超时来修复 403,也不要指望简单加 Allow: OPTIONS 就完成 CORS 授权。随后还要查看真实 POST 的响应,它也需要正确的跨域头。
若请求先跳到 HTTP、登录页或另一个域名,先让前端直达最终 API 地址。循环跳转可对照 网站重定向次数过多排查。
先决定由谁统一处理 CORS。应用框架已有中间件时,优先让应用处理,并检查 Nginx、面板和 CDN 有没有额外添加同名头。下面是另一种方案:由 Nginx 统一处理 /api/,后端保留业务鉴权。
将以下 location 合并到 API 域名已有的 HTTPS server 中,不要覆盖证书与其他站点配置。假设后端监听 127.0.0.1:3000,并且它需要保留 /api/ 路径:
location /api/ {
proxy_hide_header Access-Control-Allow-Origin;
proxy_hide_header Access-Control-Allow-Methods;
proxy_hide_header Access-Control-Allow-Headers;
proxy_hide_header Access-Control-Allow-Credentials;
add_header Access-Control-Allow-Origin "https://app.example.com" always;
add_header Access-Control-Allow-Methods "GET, POST, OPTIONS" always;
add_header Access-Control-Allow-Headers "Authorization, Content-Type" always;
if ($request_method = OPTIONS) {
return 204;
}
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_pass http://127.0.0.1:3000;
}
这个片段有几个边界:
- 来源不带路径、不带末尾斜杠;开发环境的
http://localhost:5173是另一个来源,不能直接套用生产值。来源不匹配时浏览器会拒绝读取,见 MDN 来源不匹配错误。 - 只列出真实需要的方法和头。未来要用 PATCH、DELETE 或自定义请求头,应同步调整;这份列表不替代后端对方法和权限的校验。
- OPTIONS 在这层直接返回,普通请求仍交给应用。此写法适合这条路径确实用作预检的服务;若应用用 OPTIONS 实现其他业务,应由应用区分处理。
proxy_pass没有末尾斜杠,因此保留/api/。不要为了修跨域顺手改变 URL 映射。
NGINX 响应头文档说明,always 可让错误状态响应也带上相应头。否则接口本来的 401 或 500,可能在前端表现为无法读取响应。
同时留意 add_header 的层级继承:在当前 location 新增这些指令,可能影响原本从上层继承的安全响应头,应核对最终输出。上游头的移除由 proxy_hide_header完成;如果选择应用负责 CORS,就不要照搬这个隐藏步骤。
修改后先验证语法,成功后再加载:
sudo nginx -t
sudo systemctl reload nginx
若真实鉴权位于更前面的 CDN、WAF 或其他网关,location 中的 OPTIONS 处理无法绕过那层拦截。只放行匹配业务路径的正常预检,真实请求继续认证。
如果跨来源请求需要 Cookie,前端通常要显式设置凭据模式,例如:
fetch("https://api.example.com/api/profile", {
credentials: "include"
});
服务端需要明确来源,并返回 Access-Control-Allow-Credentials: true。在前述 Nginx 方案中,应把下面一行加入同一 location 的其他 add_header 旁边:
add_header Access-Control-Allow-Credentials "true" always;
MDN 凭据与通配符错误说明指出,凭据请求不能配合 Access-Control-Allow-Origin: * 使用。不需要 Cookie 的接口,不要为了“保险”统一开启凭据。
Cookie 是否实际发送,还受 Domain、Path、Secure、SameSite 和浏览器第三方 Cookie 政策影响。跨来源不一定跨站点,不要看到两个子域就盲目改成 SameSite=None;真正跨站点的 Cookie 还涉及 Secure 和浏览器限制。跨域头修好后仍未登录,应继续检查 Cookie 与会话,而不是继续扩大允许来源。
**不要在前端加 mode: "no-cors" 来读取跨域 API。**这会得到受限制的 opaque 响应,脚本无法按普通响应读取状态和 JSON,参见 MDN Fetch 使用说明。
**不要把任意 Origin 原样回显并允许凭据。**多个可信前端应使用精确白名单;动态返回来源时还要正确设置 Vary: Origin,让缓存区分来源。不要把未经约束的字符串反射当作白名单。
不要让应用、Nginx 和 CDN 同时各写一份 Allow-Origin。MDN 多个 Allow-Origin 错误说明明确指出,不能通过重复头或逗号列表来声明多个允许来源。修改后检查最终响应,而不是只看本地配置文件。
先测允许来源的 OPTIONS 和真实请求,再用不在白名单的来源检查。对本文固定来源配置,非允许来源的预检仍可能返回 204,但 Allow-Origin 不与请求来源匹配,浏览器不会据此放行;这不是服务器返回 403 的方案。
然后检查一次未登录响应和受控的测试错误响应,确认跨域头仍只有一份。最后回到真实浏览器验证登录和业务动作。curl 能帮助观察响应头,但不会替你执行浏览器的跨域、Cookie 和缓存策略。
本地验证中,使用已安装的 Nginx 1.19.6 加模拟后端,在回环地址测试了 8 种请求组合。允许来源的 OPTIONS 返回 204,GET/POST 返回 200;模拟后端返回 401、500 时,响应仍只有一份 Allow-Origin;不在白名单的来源与固定允许值不匹配;加入凭据配置后,预检和 GET 均带有 true。OPTIONS 没有到达模拟后端。这些是 HTTP 响应头验证结果,未验证生产站点 TLS、真实登录会话或浏览器实际拦截行为。
如果应用是 FastAPI,可进一步参考 FastAPI 部署与 CORS 配置注意事项。优先缩小到失败的那一跳、明确负责加头的一层,再修改规则,通常比重装反代或关闭浏览器安全设置更容易解决问题。
