CORS 与跨域头
本页提供:Express/Nginx/CDN 三种 CORS 配置代码、OPTIONS 预检处理逻辑代码、动态 Origin 白名单实现(数据库驱动的域名验证)、凭证请求安全配置,以及预检粗判交互工具。
禁止直接反射请求 Origin;使用配置化允许域匹配(含子域策略)。Allow-Methods 与 Allow-Headers 最小化;非浏览器客户端不受 CORS 约束 — 服务端仍需鉴权。
完整 CORS 配置(Express / Nginx / CDN)
Express.js — 动态 Origin 白名单(数据库驱动)
// cors-middleware.ts
import cors from 'cors';
import { db } from './db';
// 允许的 Origin 白名单(也可从数据库/配置中心动态加载)
const STATIC_ALLOWED_ORIGINS = new Set([
'https://app.example.com',
'https://admin.example.com',
]);
// 动态白名单:从数据库验证合作伙伴域名
async function isOriginAllowed(origin: string): Promise {
if (STATIC_ALLOWED_ORIGINS.has(origin)) return true;
// 数据库驱动的域名验证(合作伙伴集成)
const allowed = await db.allowedOrigin.findUnique({
where: { origin, active: true }
});
return !!allowed;
}
export const corsMiddleware = cors({
origin: async (origin, callback) => {
// 非浏览器请求(无 Origin header)— 服务端单独鉴权
if (!origin) return callback(null, false);
const allowed = await isOriginAllowed(origin);
if (allowed) {
callback(null, origin); // 返回具体 origin,而非 *
} else {
callback(new Error(`Origin ${origin} not allowed`));
}
},
methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'Authorization', 'X-CSRF-Token'],
exposedHeaders: ['X-Request-Id'], // 仅暴露前端真正需要读取的头
credentials: true, // 需要 credentials 时使用(禁用 * origin)
maxAge: 86400, // OPTIONS 预检缓存 24 小时
});
OPTIONS 预检请求处理逻辑
// 处理 OPTIONS 预检(短路返回,不走业务逻辑)
app.options('*', corsMiddleware, (req, res) => {
// 预检响应:204 No Content
res.status(204).end();
});
// 或手动实现预检响应
app.use((req, res, next) => {
const origin = req.headers.origin;
if (req.method === 'OPTIONS') {
if (origin && STATIC_ALLOWED_ORIGINS.has(origin)) {
res.setHeader('Access-Control-Allow-Origin', origin);
res.setHeader('Access-Control-Allow-Methods', 'GET,POST,PUT,DELETE,PATCH');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type,Authorization');
res.setHeader('Access-Control-Max-Age', '86400');
res.setHeader('Vary', 'Origin'); // 防缓存投毒
return res.status(204).end();
}
return res.status(403).end();
}
next();
});
Nginx CORS 配置
# nginx.conf — CORS 配置(仅在一处声明,避免与源站重复)
server {
location /api/ {
# 白名单 origin 验证
set $cors_origin "";
if ($http_origin ~* "^https://(app|admin)\.example\.com$") {
set $cors_origin $http_origin;
}
# 预检请求处理
if ($request_method = 'OPTIONS') {
add_header 'Access-Control-Allow-Origin' $cors_origin always;
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS';
add_header 'Access-Control-Allow-Headers' 'Content-Type, Authorization';
add_header 'Access-Control-Max-Age' 86400;
add_header 'Vary' 'Origin';
return 204;
}
add_header 'Access-Control-Allow-Origin' $cors_origin always;
add_header 'Vary' 'Origin' always;
proxy_pass http://backend;
}
}
CORS 收紧主流程(skill-flow-block)
[ 浏览器跨域请求:Origin + 方法 + 自定义头 / Content-Type ]
│
▼
┌─────────────┐ 判定:简单请求 或 需 OPTIONS 预检
│ 预检与路由 │──── 网关/框架:OPTIONS 短路,返回 204 + ACAO/ACM/ACH/ACMA
└─────────────┘
│
▼
┌─────────────┐ 白名单匹配;禁止 * 反射;凭证时单一源 + ACC
│ Allow-Origin │──── Expose-Headers 最小集;Vary: Origin 防缓存投毒
└─────────────┘
│
▼
┌─────────────┐ CDN / API 网关 / 源站只在一处声明 CORS,避免重复或矛盾
│ 多层一致性 │
└─────────────┘
Agent 生成中间件时,默认输出「显式源列表 + 预检缓存时长」占位;若出现反射 Origin 或与凭证并用的 *,视为须修复项。
预检(OPTIONS)与简单请求
非简单方法(如 PUT、PATCH、DELETE)或携带非简单头 / 非简单 Content-Type(如 application/json)时,浏览器先发 OPTIONS,带 Access-Control-Request-Method 与可选的 Access-Control-Request-Headers。服务端须在预检响应中给出与真实响应一致的允许范围,否则主请求不会发出。
- 预检成功:状态常用 204;须包含匹配的
Access-Control-Allow-Origin(及凭证场景下的Access-Control-Allow-Credentials: true)。 Access-Control-Max-Age可减少 OPTIONS 流量;过长会拖慢策略变更生效,需权衡。
—
Allow-Origin / Methods / Headers
Access-Control-Allow-Methods 与 Access-Control-Allow-Headers 应覆盖预检询问的值,但不必大于业务所需。 Access-Control-Expose-Headers 默认仅暴露少量安全列表头;若前端要读自定义响应头,须显式列出。
- 通配
*与携带 Cookie /Authorization的凭证请求互斥。 - 列表维护与代码审查:新增 API 时同步收紧或扩展 CORS 配置。
凭证请求安全配置
// 前端:携带凭证的跨域请求
fetch('https://api.example.com/user', {
method: 'GET',
credentials: 'include', // 携带 Cookie
headers: { 'Authorization': 'Bearer ' + token },
});
// 服务端响应头(凭证请求必须满足以下条件)
// ✅ Access-Control-Allow-Origin 必须是具体源(不能是 *)
res.setHeader('Access-Control-Allow-Origin', 'https://app.example.com');
// ✅ 必须显式声明允许凭证
res.setHeader('Access-Control-Allow-Credentials', 'true');
// ✅ 防缓存投毒
res.setHeader('Vary', 'Origin');
// ❌ 以下组合无效(浏览器会拒绝)
// Access-Control-Allow-Origin: *
// Access-Control-Allow-Credentials: true ← 与 * 不兼容
// CDN 层:避免与源站重复声明 CORS 头
// Cloudflare Workers 示例
addEventListener('fetch', event => {
event.respondWith(handleRequest(event.request));
});
async function handleRequest(request) {
const origin = request.headers.get('Origin');
const ALLOWED = ['https://app.example.com'];
const response = await fetch(request);
const newHeaders = new Headers(response.headers);
if (origin && ALLOWED.includes(origin)) {
newHeaders.set('Access-Control-Allow-Origin', origin);
newHeaders.set('Access-Control-Allow-Credentials', 'true');
newHeaders.set('Vary', 'Origin');
// 删除源站重复的 CORS 头(避免矛盾)
newHeaders.delete('Access-Control-Allow-Origin-Duplicate');
}
return new Response(response.body, { headers: newHeaders });
}
SKILL 模板
---
name: cors-headers-hardening
description: 配置或审查 CORS:Origin 白名单、预检处理、凭证模式、动态域名验证
---
# 步骤
1. 审查 Allow-Origin:禁止反射 Origin;白名单使用 Set 或数据库驱动验证
2. credentials: true 时:Allow-Origin 必须是具体源,不能用 *
3. 添加 Vary: Origin:防止共享缓存将 A 源 CORS 头返回给 B 源
4. OPTIONS 预检短路:返回 204,不执行业务逻辑,设置 Max-Age 减少预检频率
5. Allow-Methods 最小集:只列业务真正需要的方法
6. Allow-Headers 最小集:Content-Type, Authorization, X-CSRF-Token
7. Expose-Headers:只列前端 JS 真正需要读取的响应头
8. 多层代理:CORS 头只在一处(边缘或源站)配置,避免重复或矛盾
9. Nginx 配置:用正则验证 $http_origin,设置 $cors_origin 变量
10. CDN(Cloudflare Workers):拦截后添加 CORS 头,删除源站重复头
11. 非浏览器客户端:不受 CORS 约束,服务端仍需独立鉴权
12. 测试:Chrome DevTools Network 面板区分简单请求与预检失败原因
# 禁止
- 禁止将请求 Origin 直接反射(Access-Control-Allow-Origin: req.headers.origin)
- 禁止与 credentials 并用的 *(浏览器会拒绝整个响应)
- 禁止在多层代理中重复声明 CORS 头