约 8 分钟读完 · 更新于 2026-10-21 · 文中每条都能在下面的配套工具里动手验证
CORS 是浏览器强制的同源策略,不是服务器端的鉴权机制——服务器根本没收到你被拦的那个请求。浏览器把请求分两类:满足「简单请求」条件的(GET/HEAD/POST 加少量固定头部、内容型限三种)直接发送并读响应,缺任何响应头就报错;带自定义头、JSON Content-Type、PUT/DELETE 这类会先发一个 OPTIONS 预检,只有预检通过才发真实请求。分清是哪一类,决定了你要查的是响应头还是预检链路的每一个字段。
| 控制台报错(截断) | 浏览器在等什么 | 修法 |
|---|---|---|
| No 'Access-Control-Allow-Origin' header is present | ACAO:允许的源 | 回显具体 origin,或在无凭证时用 * |
| The value of the ACAO header ... must not be the wildcard * | 不允许通配符 + 凭证 | 回显具体 origin,并加 Allow-Credentials: true |
| Request header field authorization is not allowed | ACAH 未列出该头 | 加 Access-Control-Allow-Headers,含 authorization |
| Method PUT is not allowed by Access-Control-Allow-Methods | ACAM 未列出方法 | 把该方法加进 ACAM 列表 |
| Response to preflight request doesn't pass access control check | 预检响应缺头 | OPTIONS 分支单独补齐全部头并直接 204 返回 |
| The CORS request was blocked because of a bad origin | origin 不在白名单 | 核对协议+主机+端口三段是否完全一致 |
| 预检 200 正常,真实请求被拦 | 响应在网关/反代层丢了 CORS 头 | 在最终对外的那一层补头,不要只在应用层加 |
浏览器控制台里最常见的组合错误是:代码里设了 withCredentials: true,服务器回了 *,然后浏览器直接拒绝——规范明确写了凭证模式下响应里的 origin 头不得是通配符。第二个坑是 * 只在「不带凭证」时可用,而 Cookie、Authorization 头、客户端证书都属于凭证。所以真实项目里带认证的接口几乎都必须回显具体 origin,并把值从白名单里查出来。
res.setHeader("Access-Control-Allow-Origin", "*");
res.setHeader("Access-Control-Allow-Credentials", "true");
// 前端 fetch(url, { credentials: "include" })
// 控制台:The value of the 'Access-Control-Allow-Origin' header
// in the response must not be the wildcard '*'
// when the request's credentials mode is 'include'.const ALLOW = new Set(["https://app.example.com", "https://admin.example.com"]);
app.use((req, res, next) => {
const origin = req.headers.origin;
if (origin && ALLOW.has(origin)) {
res.setHeader("Access-Control-Allow-Origin", origin); // 回显,不写 *
res.setHeader("Access-Control-Allow-Credentials", "true");
res.setHeader("Vary", "Origin"); // 关键:告诉缓存别串味
}
res.setHeader("Access-Control-Allow-Methods", "GET,POST,PUT,PATCH,DELETE,OPTIONS");
res.setHeader("Access-Control-Allow-Headers", "Content-Type,Authorization,X-Requested-With");
res.setHeader("Access-Control-Max-Age", "86400"); // 预检缓存一天
if (req.method === "OPTIONS") return res.sendStatus(204); // 预检直接结束
next();
});这是最费时间的一类症状:Network 里 OPTIONS 那一行状态码是绿的,红色标在后面的真实请求上。区别在于,预检通过只说明「你允许我发这个方法、这些头」;真实请求能否读到响应,取决于最终响应上有没有 ACAO。两个高频原因:一是 OPTIONS 分支和真实分支走了不同的代码路径(常见于框架自动加了头、手写 OPTIONS 又提前 return,或者反代把 OPTIONS 单独路由了);二是 CORS 头加在应用层,但响应真正是网关 / CDN / 反向代理吐出来的,那一层没带 CORS 头,浏览器看到的就是裸响应。排查动作:拿失败请求的 URL 直接 curl,看响应头里有没有 ACAO——没有就是在网关层丢的。
curl -i -X OPTIONS "https://api.example.com/v1/orders" \ -H "Origin: https://app.example.com" \ -H "Access-Control-Request-Method: POST" \ -H "Access-Control-Request-Headers: content-type,authorization" # 期望看到: # HTTP/1.1 204 # Access-Control-Allow-Origin: https://app.example.com # Access-Control-Allow-Methods: GET,POST,PUT,PATCH,DELETE,OPTIONS # Access-Control-Allow-Headers: content-type,authorization # Vary: Origin
location /api/ {
if ($request_method = OPTIONS) { return 204; }
add_header Access-Control-Allow-Origin $http_origin always;
add_header Access-Control-Allow-Credentials "true" always;
add_header Access-Control-Allow-Methods "GET,POST,PUT,PATCH,DELETE,OPTIONS" always;
add_header Access-Control-Allow-Headers "Content-Type,Authorization" always;
add_header Vary "Origin" always;
proxy_pass http://127.0.0.1:3000;
}
# 注意:add_header 在 if / 嵌套 location 里会失效,always 不能省当 ACAO 回显的是具体 origin 时,响应内容就随请求的 Origin 变化。如果没告诉缓存这件事,CDN、反向代理或浏览器缓存会把 A 站点拿到的响应头原样发给 B 站点——于是出现「同事那儿正常,我这儿一直失败」「刷新一下又好了」的随机现象。Vary: Origin 就是给缓存的这句说明。加了 max-age 之外的 CDN 还要注意:预检响应被缓存后,origin 白名单改动不会立刻生效,排查时用 curl 加 Cache-Control: no-cache 复测。
两种情况:一是请求带了凭证(withCredentials: include、Cookie、Authorization),规范禁止此时返回通配符,必须回显具体 origin;二是服务端在应用层加了头,但响应由网关或反代吐出,那一层没带,浏览器看到的是裸响应。用 cURL 转代码把请求重放一次,对比直连和走网关的响应头即可定位。
预检通过只代表「允许发这个方法和这些头」,真实响应能不能读,取决于最终响应上有没有 Access-Control-Allow-Origin。两个高频原因:OPTIONS 分支和真实分支走了不同代码路径(框架自动加头 + 手写 OPTIONS 提前 return,或反代把 OPTIONS 单独路由);CORS 头加在应用层但真正吐响应的是网关。curl 失败请求的 URL,看响应头里有没有这个字段,一眼就能定性。
不用。规则是「你允许什么,客户端就只能在预检里申请什么」。最省事且不会漏的做法是把客户端实际会带的自定义头列全(Content-Type、Authorization、X-Requested-With 这三个最常见),或者在服务端直接回显 Access-Control-Request-Headers——但回显等于放行任意头,安全上不如白名单。
看请求有没有到达服务端。真正的 CORS 失败在浏览器控制台里出现且 Network 面板标红,服务端访问日志里通常找不到对应记录——因为请求被浏览器拦下了。403 是服务端明确返回的拒绝,日志里能看到请求,只是响应没带或带了不匹配的 ACAO 头。两者的排查起点完全不同,先确认这一点能省掉很多弯路。