调自己的 API 报 CORS 错

浏览器 CORS 拒绝——服务端配置 / origin 不匹配。

你在前端 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” 报警,能早发现误配

相关阅读

标签: #后端 #排查 #排查 #CORS