你在前端 fetch('https://api.yourdomain.com/users'),控制台报:
Access to fetch at 'https://api.yourdomain.com/users' from origin
'https://app.yourdomain.com' has been blocked by CORS policy:
No 'Access-Control-Allow-Origin' header is present on the requested resource.
而 Postman / curl 调同一个 API 完全正常——这是 CORS 经典。CORS 是浏览器实现的安全机制,不是服务端 bug,但修是修服务端:让 server 告诉浏览器”我允许 origin X 来访问”。
理解关键:浏览器在跨 origin 请求前会先发一个 OPTIONS preflight 探路,server 必须回 Access-Control-Allow-Origin + 相关 header。preflight 错或主请求没带 header,浏览器都直接拒绝。
常见原因
按命中率从高到低:
1. 服务端根本没有发 Access-Control-Allow-Origin
最高频。新项目 server 默认不开 CORS。任何跨域请求都被拒。
如何判断:Network 看响应 header 没有 Access-Control-Allow-Origin。
2. OPTIONS preflight 没处理
POST/PUT/DELETE 或带自定义 header 的请求触发 preflight。如果 server 对 OPTIONS 返回 404 / 405,主请求永远不会发出去。
如何判断:Network 看到一个 OPTIONS 请求 status 不是 2xx。
3. Origin 字符串拼错(scheme / port / 尾斜杠)
✅ Access-Control-Allow-Origin: https://app.example.com
❌ Access-Control-Allow-Origin: app.example.com # 缺 scheme
❌ Access-Control-Allow-Origin: https://app.example.com/ # 多了 /
❌ Access-Control-Allow-Origin: http://app.example.com # http 不是 https
精确匹配,差一个字符都不行。
如何判断:Network 看响应 Access-Control-Allow-Origin 跟请求 Origin header 对比。
4. credentials: include 但 Allow-Origin 用了通配符 *
浏览器规则:带 cookie 的请求要求 Access-Control-Allow-Origin 是具体 origin 而非 *,且要带 Access-Control-Allow-Credentials: true。
如何判断:错误里含 “The value of the ‘Access-Control-Allow-Origin’ header in the response must not be the wildcard ’*‘“
5. 缺 Allow-Headers / Allow-Methods
前端发的 Content-Type: application/json 或自定义 X-API-Key header 触发 preflight。server 如果不在 Access-Control-Allow-Headers 里列出来,preflight 失败。
如何判断:错误含 “Request header field X-API-Key is not allowed”。
6. CDN / 代理层吞掉 CORS header
Cloudflare / nginx / API Gateway 有时会 strip 或覆盖响应 header。server 发出去对的,到浏览器变没的。
如何判断:直接 curl 你的 origin server(绕开 CDN)看响应,和浏览器到的对比。
最短修复路径
Step 1:服务端加 CORS 中间件
// Express
import cors from 'cors';
app.use(cors({
origin: [
'https://app.yourdomain.com',
'http://localhost:3000', // dev
],
credentials: true,
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'Authorization', 'X-API-Key'],
}));
# FastAPI
from fastapi.middleware.cors import CORSMiddleware
app.add_middleware(
CORSMiddleware,
allow_origins=["https://app.yourdomain.com", "http://localhost:3000"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
// Hono
import { cors } from 'hono/cors';
app.use('/*', cors({
origin: ['https://app.yourdomain.com', 'http://localhost:3000'],
credentials: true,
}));
Step 2:处理 OPTIONS preflight
大多数 CORS 中间件自动处理 OPTIONS。但如果你手写:
app.options('/api/*', (req, res) => {
res.set({
'Access-Control-Allow-Origin': req.headers.origin,
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
'Access-Control-Max-Age': '86400', // 缓存 24h
});
res.sendStatus(204);
});
Step 3:用 curl 验证响应
# 模拟 preflight
curl -i -X OPTIONS https://api.yourdomain.com/users \
-H "Origin: https://app.yourdomain.com" \
-H "Access-Control-Request-Method: POST" \
-H "Access-Control-Request-Headers: Content-Type"
# 应该看到:
# Access-Control-Allow-Origin: https://app.yourdomain.com
# Access-Control-Allow-Methods: ...
# Access-Control-Allow-Headers: Content-Type
Step 4:处理 credentials 场景
如果前端是 fetch(url, { credentials: 'include' }):
// 不能用 origin: '*',必须显式列举
app.use(cors({
origin: (origin, cb) => {
const allowed = ['https://app.yourdomain.com', 'http://localhost:3000'];
cb(null, allowed.includes(origin));
},
credentials: true,
}));
Step 5:CDN / 代理排查
# 直连 origin(绕 CDN)
curl -i https://origin-server-ip/api/users \
-H "Host: api.yourdomain.com" \
-H "Origin: https://app.yourdomain.com"
# 通过 CDN
curl -i https://api.yourdomain.com/users \
-H "Origin: https://app.yourdomain.com"
如果直连有 header、CDN 后没有,CDN 把它 strip 了。Cloudflare:Transform Rules → 看是否有 modify response header;nginx:检查 proxy_pass_header 是否包含 CORS header。
Step 6:浏览器开发期临时绕过
仅用于调试:
# Chrome 关 CORS 启动
open -na "Google Chrome" --args \
--user-data-dir="/tmp/chrome_dev" \
--disable-web-security
只在 dev 环境用,生产必须正确配 CORS。
预防建议
- 本地开发时把生产 origin 也加进 CORS 列表,避免上线才发现
- 用一个统一 CORS helper(不要每个 route 自己写),避免不一致
- env-driven 配置:
CORS_ORIGINS=https://app.example.com,http://localhost:3000然后 split - preflight 缓存(
Access-Control-Max-Age: 86400),减少重复 OPTIONS - 永远不要在生产用
origin: '*'+credentials: true(不仅 CORS 拒绝,也是安全隐患) - API Gateway / CDN 配置变更后,跑一次 OPTIONS curl 自检
- 监控里加一条 “CORS preflight 4xx” 报警,能早发现误配