
接口用 curl 能拿到 JSON,前端却报 CORS 错误,这并不矛盾。curl 可以展示响应;浏览器还要检查跨域读取权限。接入 CDN 后,OPTIONS 可能被拦截,响应头可能被覆盖,不同站点的跨域许可也可能共用同一份缓存。
排查时先在浏览器 Network 中确定失败的是 OPTIONS,还是实际业务请求。两者的处理层级和修复范围不同。本文面向网站前端与 API 运维,命令为 Bash/curl 示例,域名需替换,未在你的生产环境实测。
从页面来源和真实请求开始记录
Origin 由协议、主机和端口组成。例如 https://app.example.com 与 https://api.example.com 属于不同源。即使主域名相同,也可能需要跨域许可;Cookie 的同站规则与同源判断又是不同概念。[1]
在 Network 中记录页面 Origin、请求 URL、方法、请求头和凭证模式,保留 OPTIONS 与实际响应。留意重定向、状态码和请求 ID。工单中的 Cookie、Authorization、签名参数和用户数据必须脱敏。
并非每个跨域请求都会预检。符合安全列出条件的请求可能直接发送;使用 JSON Content-Type、自定义头或某些方法时通常会触发预检。浏览器的 CORS 报错也可能掩盖 CDN 返回的 403、网关 502 或登录页跳转,需要结合响应和服务端日志确认。
用 OPTIONS 复现预检,但不要把 curl 当成浏览器
下面模拟页面准备向 API 发送带 Authorization 的 JSON POST,只发送预检,不执行业务写入:
curl -sS --max-time 20 -D - -o /dev/null -X OPTIONS \
-H 'Origin: https://app.example.com' \
-H 'Access-Control-Request-Method: POST' \
-H 'Access-Control-Request-Headers: authorization,content-type' \
https://api.example.com/v1/orders
Windows 使用 curl.exe,并调整换行和输出空设备语法。请求方法和头列表应与浏览器实际发出的预检一致。curl 不执行浏览器的 CORS 判定;它的价值是查看边缘实际返回了什么。
成功预检通常返回 200 或 204,并给出匹配的 Access-Control-Allow-Origin、允许的方法和请求头。204 并不足以证明通过:缺少许可头、Origin 不匹配或 Authorization 未获允许,浏览器仍会拒绝继续。[1][2]
若 OPTIONS 被 WAF、鉴权中间件或路由拒绝,先定位产生响应的层级。标准跨源预检本身不带业务请求的 Cookie 等凭证,不应要求它先完成普通登录。但允许预检不等于允许执行订单操作,实际请求仍必须正常鉴权。不要为消除 403 直接放开整个 API 或关闭安全防护。
预检通过后,实际响应还要再次满足 CORS
浏览器接到预检许可后,才发送相应的实际请求;实际响应同样需要正确的跨域头。预检成功而业务响应遗漏许可,前端仍然无法读取结果。
可以选择无副作用、允许匿名读取的状态接口检查实际响应:
curl -sS --max-time 20 -D - -o /dev/null \
-H 'Origin: https://app.example.com' \
https://api.example.com/v1/status
这条命令只用于观察响应,不能验证订单 POST 的完整行为。不要在生产环境随意重放支付、删除或重复提交操作;业务流程应使用测试账号与隔离环境。
检查成功、401、403 和应用错误路径是否由预期层返回跨域头。对于获准的前端来源,让其能够读取自己的错误信息通常有利于诊断;但不能把所有错误响应连同私有内容开放给任意来源。CDN、反向代理和应用若都添加 Access-Control-Allow-Origin,可能形成重复字段,浏览器也可能拒绝。
如果页面需要读取自定义响应头,例如请求跟踪 ID,还要评估 Access-Control-Expose-Headers。允许发送的请求头与允许脚本读取的响应头是两份不同的名单。
携带 Cookie 时,核对精确来源与凭证许可
前端通过 credentials: "include" 等方式携带跨源 Cookie 时,服务端需要返回明确匹配的 Access-Control-Allow-Origin,以及 Access-Control-Allow-Credentials: true。此时不能用 * 代替来源。允许的方法和头也宜显式列出,Authorization 应明确获准,不能依赖通配符。[1]
Access-Control-Allow-Origin 不能写成逗号分隔的多个来源。需要支持多个站点时,在服务端维护准确的来源白名单,对合法来源返回其对应值;不要未经校验直接反射任意 Origin,也不要默认放行 null 来源。
CORS 许可不能保证 Cookie 一定被发送。SameSite、Secure、域和路径范围,以及浏览器第三方 Cookie 策略仍会影响请求。Authorization 头触发的检查也要与 Cookie 凭证问题分开分析。
跨域限制主要控制浏览器脚本读取响应,并不能替代身份认证、权限校验或 CSRF 防护。部分请求即使响应不可读,仍可能已经到达服务器并产生副作用。前端出现 CORS 报错后,不应自动无限重试写操作。
按 Origin 返回不同许可时,检查缓存是否区分变体
若应用根据白名单 Origin 返回不同 Access-Control-Allow-Origin,响应通常应包含 Vary: Origin。同时核对 CDN 的真实缓存策略:有的服务不会仅凭 Vary 自动形成你预期的缓存键,需要显式配置或对该类响应绕过缓存。[1]
典型故障是站点 A 首次请求生成带 A 许可的响应,站点 B 随后命中同一对象,却收到 A 的许可头。B 的浏览器就会拒绝读取。修复规则后,还需处理已缓存的错误响应,而不是仅更新源站代码。
若供应商支持在边缘缓存 OPTIONS,必须按其实现核对 Origin、Access-Control-Request-Method 和 Access-Control-Request-Headers 是否参与正确的变体处理。普通 HTTP 缓存并不默认缓存 OPTIONS,不能为了提高命中率就对所有方法强制共享对象。
API 含身份或用户数据时,应优先审查是否允许共享缓存。加入 Origin 维度并不能防止不同用户共用私有响应;Cookie、Authorization、Cache-Control 与缓存绕过策略仍需独立正确配置。
区分浏览器预检缓存与 CDN 对象缓存
Access-Control-Max-Age 控制浏览器保留预检许可的时间,浏览器还会施加自身上限。预检缓存与普通 HTTP 缓存分开,不能把一个字段当作 CDN 的统一 TTL。[2][3]
因此,清除 CDN 对象后,某个浏览器可能仍使用之前的预检结果;修改 Max-Age 也无法主动召回此前已保存的许可。排查时用干净浏览器环境作对照,观察是否重新发送 OPTIONS,不要把一次普通刷新视为全部状态已清空。
权限收紧必须在实际业务请求上立即执行,不能依赖旧预检许可过期来保护接口。调试期可评估较短的预检缓存时间,确认规则后再按性能和安全需求调整。
用小范围变更验证,并保留回滚记录
修改前备份来源白名单、CDN 缓存规则、WAF 配置和响应头策略。选定测试域名或低风险路径验证,避免一次性对全站开放跨域。
验收至少包含:合法来源与非法来源、无凭证与带凭证、预检和实际请求、成功和错误响应,以及两个合法来源交替访问时的缓存行为。浏览器结果、边缘响应与应用日志应能对应同一请求。
若出现 301/302 等跳转,检查是否跳到了登录页、另一域名或不同协议;预检重定向的处理存在兼容性差异,宜让客户端直接请求预期 API 端点,避免依赖复杂跳转。混合内容、证书故障和网络不可达也需要单独修复。
回滚时恢复上一份经过验证的策略,按影响范围处理边缘旧对象,并再次检查现有浏览器会话。不要通过 mode: "no-cors" 掩盖故障:它通常只会得到脚本无法读取的 opaque 响应,无法让前端正常读取 JSON。
工单里留下失败阶段、返回该响应的层级、Origin 与许可头的对应关系,以及缓存命中情况。这样可以明确下一步该改路由、鉴权、响应头,还是缓存策略。
参考资料
资料核验日期:2026 年 10 月 28 日。供应商的 OPTIONS 转发和边缘缓存行为需查当前平台文档。