推荐配置模板:从简单到严格

配置模板不是复制后永久不改的答案。应先确定部署拓扑、认证方式和资源类型,再选择最接近的起点。

模板 A:本地开发

import cors, { type CorsOptions } from "cors";
import express from "express";
import helmet from "helmet";

const app = express();

const corsOptions: CorsOptions = {
  origin: "http://localhost:5173",
  allowedHeaders: ["Content-Type", "Authorization"],
  exposedHeaders: ["Content-Disposition", "X-Request-Id"],
};

app.use(
  helmet({
    // 当前应用只返回 JSON;页面 CSP 由前端开发服务器负责。
    contentSecurityPolicy: false,
  }),
);
app.use(cors(corsOptions));
app.use(express.json());

关闭 API 响应上的 CSP 不等于关闭所有 Helmet 防护,也不代表生产页面应关闭 CSP。

模板 B:同源生产部署

前端通过 /api 访问同一 Origin,一般不需要 cors()

const app = express();

app.set("trust proxy", 1);
app.use(helmet());
app.use(express.json({ limit: "1mb" }));

页面的 CSP 和 HSTS 放在提供 HTML 和终止 TLS 的 Nginx。若 API 还被其他 Origin 访问,则不能套用该模板。

模板 C:跨子域 + Bearer Token

const allowedOrigins = new Set([
  "https://www.example.com",
  "https://admin.example.com",
]);

const corsOptions: CorsOptions = {
  origin(origin, callback) {
    if (origin === undefined || allowedOrigins.has(origin)) {
      callback(null, true);
      return;
    }

    callback(new Error("Origin 不在允许列表中"));
  },
  methods: ["GET", "POST", "PUT", "PATCH", "DELETE"],
  allowedHeaders: ["Content-Type", "Authorization"],
  exposedHeaders: ["Content-Disposition", "X-Request-Id"],
  maxAge: 600,
};

app.use(helmet());
app.use(cors(corsOptions));

允许 origin === undefined 是为了保留服务间调用、curl 和健康检查。它不是认证放行:所有接口仍要执行身份认证和授权。

不要用下面这种判断:

// 错误示意:www.example.com.attacker.test 也可能被误判
origin.includes("example.com");

模板 D:跨源 Cookie 登录

const corsOptions: CorsOptions = {
  origin: "https://www.example.com",
  credentials: true,
  methods: ["GET", "POST", "PUT", "PATCH", "DELETE"],
  allowedHeaders: ["Content-Type", "X-CSRF-Token"],
  maxAge: 600,
};

app.use(helmet());
app.use(cors(corsOptions));

必要条件:

模板 E:逐步启用 CSP

第一阶段使用 Report-Only,不阻止页面资源:

app.use(
  helmet.contentSecurityPolicy({
    reportOnly: true,
    directives: {
      defaultSrc: ["'self'"],
      scriptSrc: ["'self'"],
      styleSrc: ["'self'"],
      imgSrc: ["'self'", "data:"],
      connectSrc: ["'self'", "https://api.example.com"],
      objectSrc: ["'none'"],
      baseUri: ["'self'"],
      frameAncestors: ["'none'"],
    },
  }),
);

收集并修正违规后,再移除 reportOnly: true。CSP 应设置在 HTML 文档响应上;若 HTML 来自 Nginx,应在那里配置,或让 HTML 经过应用层统一生成。

中间件顺序模板

app.use(requestIdMiddleware);
app.use(helmet());
app.use(cors(corsOptions));
app.use(express.json());
app.use(authenticationMiddleware);
app.use("/api", apiRouter);
app.use(notFoundHandler);
app.use(errorHandler);

关键目标不是背诵顺序,而是确保:

环境变量白名单

CORS_ALLOWED_ORIGINS=https://www.example.com,https://admin.example.com
const allowedOrigins = new Set(
  (process.env.CORS_ALLOWED_ORIGINS ?? "")
    .split(",")
    .map((origin) => origin.trim())
    .filter(Boolean),
);

启动时应校验生产环境白名单非空且均为合法 URL。不要在请求期间反复解析配置,也不要因为配置缺失自动退化成 *