前端请求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缓存键、 |
哪些请求会触发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.comAPI地址是:
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就可能发生这样的情况:
A后台先访问API。
CDN缓存了带有A域名的响应。
B后台随后请求相同URL。
CDN把A的响应直接返回给B。
B浏览器发现允许的Origin不匹配,于是报CORS错误。
这类问题的特点是:
刷新缓存后暂时恢复
第一个访问的域名正常,另一个域名失败
问题不是每次都出现
不同CDN节点结果不一样
响应中的
Access-Control-Allow-Origin会变化
修复时需要确认:
源站返回
Vary: OriginCDN支持并正确遵守
VaryCDN缓存键是否需要包含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被拦截