推荐配置模板:从简单到严格
配置模板不是复制后永久不改的答案。应先确定部署拓扑、认证方式和资源类型,再选择最接近的起点。
模板 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));
必要条件:
- 前端使用
credentials: "include"或 AxioswithCredentials: true。 - 不能使用
Access-Control-Allow-Origin: *。 - 跨站 Cookie 通常需要
SameSite=None; Secure。 - 仍需 CSRF 防护,特别是具有写入行为的接口。
- 登录 Cookie 建议
HttpOnly,避免前端脚本读取。
模板 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);
关键目标不是背诵顺序,而是确保:
- OPTIONS 不会先被 JWT 认证拒绝。
- 错误响应也带必要的 CORS Header。
- 解析请求体之前已完成通用 Header 设置。
- 安全 Header 不因某个路由提前响应而缺失。
环境变量白名单
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。不要在请求期间反复解析配置,也不要因为配置缺失自动退化成 *。