接口用 curl 正常,浏览器却报跨域?CORS 预检、凭据与响应头排查

接口使用 curl 可以返回数据,浏览器却报 CORS 错误,需要分别检查 OPTIONS 预检和实际响应。本文从 Network 面板入手,说明允许来源、请求头、凭据与 Cookie 的匹配规则,排查错误响应缺头、代理重复加头和 CDN 缓存干扰,并给出安全修改与浏览器验收方法,避免通配符放行或关闭安全策略。
接口用 curl 正常,浏览器却报跨域?CORS 预检、凭据与响应头排查

同一个 API,curl 能返回 JSON,前端却提示 CORS 错误,首先要检查浏览器看到的响应头,以及请求前是否发送了 OPTIONS 预检。curl 不执行浏览器的同源策略,命令行成功只能证明这次网络请求拿到了响应,不能证明网页脚本有权限读取它。

另一个容易误判的地方是错误响应。接口实际返回了 401、502 或重定向,但这些响应没有正确的跨域头,浏览器最终把问题显示成跨域失败。直接给所有来源开放访问,会掩盖原来的鉴权或上游故障,还可能增加风险。

本文适用于普通网页通过 Fetch 或 XMLHttpRequest 访问 HTTP API 的场景。示例域名需要替换为自己的测试域名;以下操作没有在读者的生产环境执行。对象存储授权、浏览器扩展和本地文件页面需要结合各自规则另行判断。

从 Network 面板区分预检与实际请求

浏览器的“源”由协议、主机和端口共同确定。https://app.example.com 与 https://api.example.com 不同源;同一主机换端口或从 HTTP 换成 HTTPS,也会产生跨源请求。

打开开发者工具的 Network 面板,保留日志后重试,记录请求 URL、Origin、状态码和响应头。按实际看到的请求判断:

现象 优先检查
OPTIONS 返回 401、403、404 或跳到登录页 预检被鉴权、WAF、路由或重定向拦截
OPTIONS 成功,但没有后续请求 允许来源、方法、请求头是否匹配
实际请求返回 200,脚本仍无法读取 实际响应的跨域头、凭据规则、重复响应头
实际请求返回 401、500 或 502 先定位原始业务或代理故障,同时核对错误响应头

控制台提示有价值,但不能代替完整请求链路。TLS 证书错误、DNS 失败和混合内容阻止,也会让 Fetch 报网络错误;这些问题需要分别处理。

JSON、Authorization 为什么容易触发 OPTIONS

只有符合“简单请求”条件的跨源请求才不需要预检。常见的 Content-Type: application/json、Authorization 或自定义请求头,会让浏览器先询问服务器是否允许相关方法和请求头。PUT、DELETE 等方法通常也需要预检。

下面用 curl 模拟一次预检,注意它只是帮助观察响应,不会替浏览器执行完整校验:

curl -sS --connect-timeout 5 --max-time 15 -D - -o /dev/null \
  -X OPTIONS 'https://api.example.com/v1/profile' \
  -H 'Origin: https://app.example.com' \
  -H 'Access-Control-Request-Method: POST' \
  -H 'Access-Control-Request-Headers: content-type,authorization'

该命令没有附带用户凭据。普通 CORS 预检本身不携带用户认证信息,服务器不应要求它先完成常规会话登录。生产环境应按路由和方法识别有效预检,并继续对后续实际请求执行身份验证与权限检查。放行预检不会替代实际接口授权。

状态码为 204 或其他成功状态,还不足以说明通过。返回头需要明确允许请求来源、POST 方法,以及将要使用的 content-type、authorization。只写 Allow: GET, POST 也不等于设置了 Access-Control-Allow-Methods。

预检通过后,实际响应也要有跨域头

对仅允许一个前端站点、需要凭据的 API,一组示例响应头如下:

Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Credentials: true
Vary: Origin

预检响应还需要与实际请求匹配的允许项,例如:

Access-Control-Allow-Methods: GET, POST, OPTIONS
Access-Control-Allow-Headers: Content-Type, Authorization

这些都是示例,不能照抄到不需要对应权限的接口。允许来源必须是一个匹配的源,不带路径;不能用逗号把多个来源拼成一个 Access-Control-Allow-Origin。如果要支持多个站点,应在服务端用严格白名单判断后返回一个匹配来源,而不是把任意 Origin 原样反射回去。

跨域逻辑最好由应用或一个明确的网关层负责。如果应用已经返回 Access-Control-Allow-Origin,Nginx 又添加一次,重复头可能让浏览器拒绝响应。排查时应读取最终经过代理、CDN 后的结果。

Vary: Origin 用于告知缓存响应会随 Origin 改变。已有 Vary: Accept-Encoding 等值时应正确合并,不能为了跨域把原值覆盖掉;CDN 还需要支持并正确配置相应缓存行为,单加一个头不保证所有平台都按预期区分缓存。

带 Cookie 时,通配符为什么不行

前端需要跨源携带 Cookie 时,Fetch 通常要显式设置:

fetch('https://api.example.com/v1/profile', {
  credentials: 'include'
});

服务端同时需要返回匹配的明确来源,以及 Access-Control-Allow-Credentials: true。凭据模式下不能用 Access-Control-Allow-Origin: *。Authorization 请求头也应在预检允许头中显式列出,不能依赖通配符来处理它。

Cookie 是否发送还取决于 Domain、Path、Secure、SameSite 以及浏览器第三方 Cookie 策略。跨源不一定跨站:两个子域可以跨源但仍属于同站。只有在确实需要跨站 Cookie 的部署中,才评估 SameSite=None; Secure,并同时落实 CSRF 防护,不能把修改 Cookie 属性当作通用跨域修复。

CORS 控制浏览器脚本读取响应的权限,无法阻止所有跨站请求发出。没有预检的请求也可能到达服务器,所以服务端授权、CSRF 防护和输入验证仍然不可缺少。

前端把模式改成 no-cors 同样无法解决需要读取 JSON 的业务:返回的 opaque 响应不能被脚本正常读取内容。关闭浏览器安全策略,也只能制造一个不代表真实用户环境的测试结果。

为什么只有报错时才显示跨域失败

应用正常响应带了跨域头,鉴权中间件或反向代理生成的错误响应没有带,就会出现“成功时正常、出错时跨域”的现象。要同时查看服务端日志中的原始状态,以及浏览器请求对应的时间和请求标识。

Nginx 的 add_header 默认只针对特定状态码添加响应头;使用 always 可以扩展到其他状态,但它不会自动完成来源白名单、预检处理或缓存配置。不要只给服务器随意加一行 add_header Access-Control-Allow-Origin * always; 就认为问题已解决。

还要检查配置继承:子级 location 定义了自己的 add_header 时,传统继承行为可能使上级头不再继承。较新版本还有额外继承控制能力,需按部署版本与实际生效配置判断。

涉及 Nginx 配置修改时,先备份相关站点文件,确认正在运行实例所用的路径,再执行 nginx -t。语法通过后,在维护安排下平滑重载;出现新故障时恢复备份并重新检查。修改不会修复本来就存在的上游超时或失效凭据。

CDN 与预检缓存会让修改看起来没生效

如果接口根据不同 Origin 返回不同响应,缓存策略必须能区分这些变化。对个性化、带凭据的响应,应按业务设置禁止共享缓存或严格隔离,不能只修跨域头而忽略用户数据的缓存边界。

预检还可能在浏览器自己的预检缓存中停留,Access-Control-Max-Age 控制相关缓存时间,并受浏览器上限影响。调整策略时可先在受控测试环境采用短时间值,并使用新的浏览器会话复查;不要把 Network 面板禁用普通缓存等同于清除了所有预检缓存。

如果 OPTIONS 到达边缘节点后被 WAF 拦截,需要只针对确认的 API 路径、来源和方法调整规则。不能为了测试全站关闭 WAF,也不能给所有任意来源授予凭据访问权限。

验收时同时检查允许和拒绝的场景

至少验证允许来源的预检、实际请求,以及未授权来源的拒绝结果。拒绝结果可以没有允许跨域头,但实际接口仍需依靠身份鉴别和权限校验保护数据,不能把来源校验当作鉴权。

再检查一次过期登录、无权限、应用错误和代理错误:浏览器能否在允许的来源下正确读到业务错误,服务端是否记录了原始状态。需要由前端读取自定义响应头时,还应按需设置 Access-Control-Expose-Headers;允许发送哪些请求头与允许读取哪些响应头是两件事。

最终应在真实浏览器中完成一次业务验证。curl 适合定位链路和头部,预检与实际请求都符合规则之后,才算解决了浏览器跨域访问的问题。

官方参考

资料核验日期:2026 年 10 月 21 日。

Web服务与建站

WordPress 首页正常,文章页全是 404?Nginx 与 Apache 固定链接排查

2026-10-20 14:11:15

实操指南

服务器加固指南:增强系统安全的实用措施

2024-11-4 9:20:25