CORS 与跨域头

本页提供:Express/Nginx/CDN 三种 CORS 配置代码、OPTIONS 预检处理逻辑代码、动态 Origin 白名单实现(数据库驱动的域名验证)、凭证请求安全配置,以及预检粗判交互工具。

禁止直接反射请求 Origin;使用配置化允许域匹配(含子域策略)。Allow-MethodsAllow-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)与简单请求

非简单方法(如 PUTPATCHDELETE)或携带非简单头 / 非简单 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 流量;过长会拖慢策略变更生效,需权衡。
预检粗判(本页 cors-page 脚本)

Allow-Origin / Methods / Headers

Access-Control-Allow-MethodsAccess-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 头

返回技能库 更多技能入口