返回博客列表

前端请求API被CORS拦截?这样检查CDN响应头和预检请求

CdnChart 技术团队发布于 2026-10-0212 分钟阅读
前端请求API被CORS拦截?这样检查CDN响应头和预检请求

前端页面直接请求API,浏览器控制台突然出现一片红色错误:

Access to fetch at 'https://api.example.com/user'
from origin 'https://www.example.com'
has been blocked by CORS policy

很多人看到“blocked”这个词,第一反应是CDN或者防火墙把请求拦截了。

其实,CORS报错不一定代表请求没有到达服务器。

有时候API已经正常执行,服务器也返回了200,只是响应中缺少浏览器要求的跨域头,所以JavaScript无法读取结果。还有一些情况是浏览器先发送了OPTIONS预检请求,但预检没有通过,真正的GET、POST请求根本没有发出去。

排查CORS问题,不能只看API是否“能打开”,而要分别检查:

  • 浏览器发送了什么请求

  • OPTIONS预检是否成功

  • 实际API响应是否包含跨域头

  • 响应头由源站还是CDN添加

  • CDN是否缓存了其他Origin的响应

  • 错误响应是否也带有CORS头

先弄清楚:CORS不是普通的网络拦截

CORS的全称是Cross-Origin Resource Sharing,即跨源资源共享。

浏览器会根据页面来源和API地址判断请求是否跨源。一个Origin由三部分组成:

协议 + 域名 + 端口

下面这些地址互相都可能属于不同的Origin:

https://www.example.com
https://api.example.com
http://www.example.com
https://www.example.com:8443

即使两个域名属于同一家公司,www.example.com和api.example.com在浏览器看来仍然是不同的Origin。

CORS主要限制浏览器中的JavaScript读取跨源响应。它不等于服务器拒绝请求,也不等于CDN一定中断了连接。

因此,经常会出现这种情况:

  • 浏览器调用失败

  • 在地址栏打开API正常

  • curl请求正常

  • Postman请求也正常

  • 服务器访问日志里能看到200

这并不矛盾。curl和Postman通常不会像浏览器一样执行CORS检查,所以它们请求成功,只能证明API可以访问,不能证明CORS配置正确。

先在浏览器里看失败的是OPTIONS还是实际请求

打开Chrome或Edge开发者工具:

F12 → Network → 重新发起请求

在请求列表中查找API地址,重点看是否出现了两条请求:

OPTIONS /api/user
POST /api/user

如果只有OPTIONS,没有后面的POST,通常说明预检请求没有通过。

如果OPTIONS成功,POST也发出去了,但浏览器仍提示CORS错误,说明实际响应可能缺少正确的跨域头。

可以按照下面的情况快速判断:

浏览器现象

优先检查

只有OPTIONS,没有实际请求

预检状态码、允许方法、允许请求头

OPTIONS返回403

CDN、WAF或源站禁止OPTIONS

OPTIONS返回404

API路由没有处理OPTIONS

OPTIONS返回301或302

HTTP跳HTTPS、域名跳转或路径重定向

OPTIONS返回200,但仍报错

响应头内容是否完整

实际请求返回200,但前端读不到

实际响应缺少CORS头

实际请求返回401或403并提示CORS

错误响应没有携带CORS头

偶尔成功、偶尔失败

CDN缓存键、Vary: Origin或多源站不一致

哪些请求会触发OPTIONS预检?

不是所有跨域请求都会先发送OPTIONS。

普通GET请求,如果没有特殊请求头,可能直接发送;但下面这些情况更容易触发预检:

  • 使用PUT、PATCH、DELETE等方法

  • POST使用application/json

  • 请求带有Authorization

  • 请求带有自定义请求头

  • 请求包含浏览器安全列表以外的Content-Type

  • 前端主动添加特殊Header

例如:

fetch("https://api.example.com/user", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "Authorization": "Bearer token"
  },
  body: JSON.stringify({
    name: "test"
  })
});

浏览器通常不会马上发送POST,而是先询问服务器:

OPTIONS /user HTTP/1.1
Origin: https://www.example.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: authorization, content-type

只有预检响应允许这个Origin、方法和请求头,浏览器才会继续发送真正的POST请求。

MDN的CORS说明也指出,非简单跨源请求通常需要先通过预检,预检成功以后浏览器才会发送实际请求。

用curl模拟浏览器的OPTIONS预检

假设前端页面是:

https://www.example.com

API地址是:

https://api.example.com/v1/user

请求方法是POST,并携带Authorization和Content-Type。

可以这样模拟预检:

curl -i -X OPTIONS \
  'https://api.example.com/v1/user' \
  -H 'Origin: https://www.example.com' \
  -H 'Access-Control-Request-Method: POST' \
  -H 'Access-Control-Request-Headers: authorization,content-type'

一个正常的预检响应可能类似:

HTTP/2 204
Access-Control-Allow-Origin: https://www.example.com
Access-Control-Allow-Methods: GET, POST, OPTIONS
Access-Control-Allow-Headers: Authorization, Content-Type
Access-Control-Allow-Credentials: true
Access-Control-Max-Age: 600
Vary: Origin

需要逐项检查。

Access-Control-Allow-Origin

它决定哪个Origin可以读取响应:

Access-Control-Allow-Origin: https://www.example.com

如果返回的是:

Access-Control-Allow-Origin: https://admin.example.com

而实际页面来自:

https://www.example.com

两者不一致,浏览器仍会拒绝。

Origin必须完整匹配,包括协议和端口:

http://www.example.com
https://www.example.com
https://www.example.com:8443

这是三个不同的Origin。

Access-Control-Allow-Methods

它应该包含实际请求使用的方法:

Access-Control-Allow-Methods: GET, POST, OPTIONS

如果前端发送DELETE,而响应里只有GET和POST,预检不会通过。

Access-Control-Allow-Headers

它应该包含前端实际准备发送的非简单请求头:

Access-Control-Allow-Headers: Authorization, Content-Type

如果前端发送了:

X-Request-ID
X-App-Version
Authorization

但预检响应没有允许这些Header,浏览器会阻止后续请求。

Access-Control-Allow-Credentials

如果前端需要跨域携带Cookie或HTTP认证信息,需要:

Access-Control-Allow-Credentials: true

而且前端请求还要明确启用凭据:

fetch("https://api.example.com/user", {
  credentials: "include"
});

需要注意,带凭据的跨域请求不能使用:

Access-Control-Allow-Origin: *

这时必须返回明确的Origin,例如:

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

如果凭据请求和通配符*同时使用,浏览器会拒绝响应。

Vary: Origin

如果服务器会根据请求中的Origin动态返回不同的Access-Control-Allow-Origin,还应该返回:

Vary: Origin

它告诉浏览器、CDN和其他缓存系统:来自不同Origin的请求,响应可能不一样。

MDN建议,在返回具体Origin而不是*时,同时使用Vary: Origin,避免不同Origin之间错误复用缓存响应。

OPTIONS成功,不代表实际请求一定成功

预检响应正确后,还要检查真正的API响应。

例如测试GET:

curl -i \
  'https://api.example.com/v1/user' \
  -H 'Origin: https://www.example.com'

测试带JSON的POST:

curl -i \
  'https://api.example.com/v1/user' \
  -X POST \
  -H 'Origin: https://www.example.com' \
  -H 'Content-Type: application/json' \
  -H 'Authorization: Bearer test-token' \
  --data '{"name":"test"}'

实际响应也必须包含相应的CORS头:

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

一个很常见的配置错误是:

OPTIONS响应带有CORS头
POST响应没有CORS头

浏览器虽然通过了预检,也发送了POST,但最终仍不允许前端JavaScript读取响应。

反过来也一样:实际响应配置正确,OPTIONS没有正确处理,真正的请求同样发不出去。

重点检查CDN有没有改写或删除响应头

如果API没有经过CDN时跨域正常,接入CDN以后开始报错,就要分别测试CDN和源站。

假设源站IP是:

203.0.113.10

测试经过CDN的响应:

curl -i \
  'https://api.example.com/v1/user' \
  -H 'Origin: https://www.example.com'

绕过CDN访问源站,同时保留正确的Host和HTTPS SNI:

curl -i \
  --resolve api.example.com:443:203.0.113.10 \
  'https://api.example.com/v1/user' \
  -H 'Origin: https://www.example.com'

预检也要分别测试:

curl -i -X OPTIONS \
  --resolve api.example.com:443:203.0.113.10 \
  'https://api.example.com/v1/user' \
  -H 'Origin: https://www.example.com' \
  -H 'Access-Control-Request-Method: POST' \
  -H 'Access-Control-Request-Headers: authorization,content-type'

对比两组结果:

CDN响应

源站响应

更可能的问题

CDN缺少CORS头

源站有正确CORS头

CDN删除、覆盖或缓存了响应头

CDN和源站都缺少

源站应用或Web服务器配置

CDN出现重复头

源站和CDN同时添加

CDN返回403,源站返回204

WAF或边缘规则拦截OPTIONS

CDN返回旧Origin

CDN缓存没有区分Origin

两边头部相同,浏览器仍失败

检查凭据、重定向和前端实际Origin

还可以通过CDNChart的CDN检测工具确认API域名是否经过CDN,以及当前解析到的CNAME和节点IP。

如果问题只在部分地区出现,可以使用网站测速工具观察不同地区的状态码和节点结果,再从浏览器或命令行进一步检查具体响应头。

最容易忽略的问题:CORS响应被CDN缓存错了

假设API允许两个后台域名访问:

https://admin-a.example.com
https://admin-b.example.com

源站根据请求中的Origin动态返回:

Access-Control-Allow-Origin: https://admin-a.example.com

或者:

Access-Control-Allow-Origin: https://admin-b.example.com

如果CDN缓存键里没有Origin,又没有正确处理:

Vary: Origin

就可能发生这样的情况:

  1. A后台先访问API。

  2. CDN缓存了带有A域名的响应。

  3. B后台随后请求相同URL。

  4. CDN把A的响应直接返回给B。

  5. B浏览器发现允许的Origin不匹配,于是报CORS错误。

这类问题的特点是:

  • 刷新缓存后暂时恢复

  • 第一个访问的域名正常,另一个域名失败

  • 问题不是每次都出现

  • 不同CDN节点结果不一样

  • 响应中的Access-Control-Allow-Origin会变化

修复时需要确认:

  • 源站返回Vary: Origin

  • CDN支持并正确遵守Vary

  • CDN缓存键是否需要包含Origin

  • API响应是否真的适合缓存

  • OPTIONS响应是否被错误地长期缓存

  • 多个允许域名是否配置一致

不要简单把请求中的Origin原样反射到响应头,尤其是在允许Cookie或认证信息时。应该先把Origin与明确的白名单进行比较,通过后再返回对应Origin。

重复的Access-Control-Allow-Origin也会失败

有些团队在源站已经配置了:

Access-Control-Allow-Origin: https://www.example.com

后来又在CDN控制台添加了一次相同响应头。

最终浏览器收到的可能是:

Access-Control-Allow-Origin: https://www.example.com
Access-Control-Allow-Origin: *

或者:

Access-Control-Allow-Origin: https://www.example.com, *

这不是“允许范围更大”,而是无效配置。

Access-Control-Allow-Origin应该只返回一个有效值。MDN也指出,响应包含多个该字段时,可能出现Origin不匹配错误。

使用下面的命令可以直接查看是否存在重复头:

curl -sS -D - -o /dev/null \
  'https://api.example.com/v1/user' \
  -H 'Origin: https://www.example.com'

如果源站和CDN都在添加CORS头,建议只保留一个明确的配置层。否则后续修改时,很容易出现两边规则不一致。

错误响应同样需要CORS头

还有一种让人很难受的情况:

API真正返回的是401、403、429或500,但因为错误响应没有CORS头,浏览器只显示CORS错误,前端看不到原始错误内容。

例如API返回:

HTTP/2 401 Unauthorized
Content-Type: application/json

但缺少:

Access-Control-Allow-Origin: https://www.example.com

前端JavaScript可能无法读取401响应正文,只能看到模糊的CORS提示。

因此,要检查CORS头是否覆盖:

  • 2xx成功响应

  • 4xx鉴权和限流响应

  • 5xx服务器错误

  • WAF拦截页面

  • CDN自定义错误页

  • Nginx直接返回的错误

  • 应用异常处理器返回的错误

如果使用Nginx添加响应头,可以根据实际需求检查是否需要使用always参数:

add_header Access-Control-Allow-Origin "https://www.example.com" always;
add_header Access-Control-Allow-Credentials "true" always;
add_header Vary "Origin" always;

但这只是单一允许域名的示例。多域名场景应使用严格白名单,不要在带凭据的API上无条件放行任意Origin。

WAF可能允许GET,却拦截OPTIONS

一些安全规则只允许常见的GET和POST,却把OPTIONS识别为异常方法。

表现通常是:

GET接口直接访问正常
OPTIONS返回403
浏览器不发送后续POST
Postman调用却成功

这时需要检查:

  • CDN是否允许OPTIONS方法

  • WAF是否拦截OPTIONS

  • 源站路由是否处理OPTIONS

  • 负载均衡器是否转发OPTIONS

  • API网关是否为OPTIONS配置了路由

  • 鉴权中间件是否错误地要求预检请求登录

  • 防盗链规则是否检查了错误的Referer或Origin

预检请求本身通常不会携带实际业务请求中的认证凭据。不能要求OPTIONS必须先通过用户登录认证,否则浏览器可能永远无法发送真正的请求。

重定向也可能让预检失败

检查OPTIONS响应时,还要留意:

301 Moved Permanently
302 Found
307 Temporary Redirect
308 Permanent Redirect

常见的重定向包括:

http://api.example.com → https://api.example.com
api.example.com/v1 → api.example.com/v1/
旧域名 → 新域名
无www → 有www

浏览器对跨源预检和重定向的处理比普通页面请求严格。即使最终地址配置了正确的CORS头,中间跳转仍可能让请求失败。

前端API地址最好直接填写最终URL,不要依赖多次重定向。

可以使用:

curl -i -X OPTIONS \
  'https://api.example.com/v1/user' \
  -H 'Origin: https://www.example.com' \
  -H 'Access-Control-Request-Method: POST'

如果看到Location响应头,就要继续检查前端使用的URL是否需要修改。

修改CORS配置后,为什么浏览器还在报错?

浏览器本身可能缓存预检结果,CDN也可能缓存OPTIONS或API响应。

预检响应中常见的缓存控制是:

Access-Control-Max-Age: 600

它表示浏览器可以在一段时间内复用预检结果。Fetch标准中也定义了浏览器的CORS预检缓存。

配置修改后,可以尝试:

  • 清除浏览器缓存

  • 使用无痕窗口重新测试

  • 在开发者工具中禁用缓存

  • 清理CDN对应URL缓存

  • 检查OPTIONS是否单独缓存

  • 更换节点或网络测试

  • 确认实际请求没有命中旧响应

但不要把“清缓存”当成最终解决方案。如果缓存键和Vary: Origin本身配置错误,问题还会再次出现。

一次完整的CORS排查应该保留什么?

为了让前端、后端和CDN技术人员看到同一份证据,建议保存:

前端页面完整Origin
API完整URL
实际请求方法
实际请求头
OPTIONS状态码
OPTIONS响应头
实际请求状态码
实际响应头
CDN节点IP
源站IP
请求发生时间
CDN请求ID
浏览器控制台完整报错

尤其不要只说“跨域了”。

“跨域了”只能说明浏览器拒绝读取响应,不能说明是哪一个字段错误。把OPTIONS和实际请求的响应头完整贴出来,问题通常很快就能缩小到具体配置。

常见问题

API在Postman里正常,为什么浏览器还是失败?

因为Postman通常不会执行浏览器的同源策略和CORS检查。Postman成功只能证明API可访问,不能证明API返回了正确的跨域响应头。

Access-Control-Allow-Origin设置成*最省事吗?

只适合公开、无凭据的资源。如果请求携带Cookie、HTTP认证或前端使用credentials: "include",就不能使用*,必须返回明确的允许Origin。

OPTIONS返回204正常吗?

正常。预检请求不需要业务响应正文,返回200或204都可以。重点是状态码成功,并且包含正确的允许Origin、方法和请求头。

为什么只有登录后的请求出现CORS错误?

登录后的请求通常会携带Cookie或认证信息,需要同时正确配置Access-Control-Allow-Credentials: true,并返回明确的Origin,不能继续使用*。

为什么只有报错时出现CORS问题?

很可能是401、403、429或500响应没有携带CORS头。成功响应配置正确,不代表错误响应也配置正确。

CORS应该配置在CDN还是源站?

两种方式都能实现,但最好选择一个主要配置层。API权限逻辑通常更适合由源站或API网关管理;CDN可以补充统一响应头,但要避免源站和CDN重复添加、覆盖或缓存错误。

  • API跨域
  • CORS错误
  • CDN跨域配置
  • Access-Control-Allow-Origin
  • OPTIONS预检请求
  • CDN响应头
  • 前端请求API被拦截