08 Express 5 中配置 cors

1. 安装与类型

npm install cors
npm install --save-dev @types/cors

cors 是运行时依赖。它目前通常通过 @types/cors 提供 TypeScript 类型;安装前也可检查包版本是否已内置类型。

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

const app = express();

2. 最宽松的配置

app.use(cors());

这通常返回 Access-Control-Allow-Origin: *,适合公开、无浏览器凭据的 API 示例,不建议不加思考地用于管理后台。

3. 允许一个固定来源

const corsOptions: CorsOptions = {
  origin: "http://localhost:5173",
};

app.use(cors(corsOptions));

Origin 必须精确匹配协议、主机和端口,末尾不要添加路径。

4. 多环境白名单

const allowedOrigins = new Set([
  "http://localhost:5173",
  "https://test.example.com",
  "https://www.example.com",
]);

const corsOptions: CorsOptions = {
  origin(origin, callback) {
    if (origin === undefined) {
      // curl、健康检查、服务间请求等可能没有 Origin。
      callback(null, true);
      return;
    }

    if (allowedOrigins.has(origin)) {
      callback(null, true);
      return;
    }

    callback(new Error(`不允许的 Origin: ${origin}`));
  },
};

不要使用 origin.includes("example.com")https://example.com.attacker.test 也可能通过。需要支持子域时,应使用 URL 解析并严格校验协议和 hostname。

白名单适合从环境变量读取,但要在服务启动时解析并验证,避免拼写错误上线后才暴露。

5. Cookie 跨域配置

const corsOptions: CorsOptions = {
  origin: "https://www.example.com",
  credentials: true,
};

前端还要主动开启凭据模式。CORS 只是其中一步;Cookie 通常还需要:

Secure
SameSite=None(确实跨站时)
合理的 Domain 和 Path
CSRF 防护

如果使用 Bearer Token,不要仅因为“有登录”就盲目开启 credentials

6. 完整配置示例

const corsOptions: CorsOptions = {
  origin: allowedOriginsArray,
  methods: ["GET", "HEAD", "POST", "PUT", "PATCH", "DELETE"],
  allowedHeaders: ["Content-Type", "Authorization", "X-Request-Id"],
  exposedHeaders: ["Content-Disposition", "X-Request-Id", "X-Total-Count"],
  credentials: false,
  maxAge: 600,
  optionsSuccessStatus: 204,
};

app.use(cors(corsOptions));

不要为了“保险”列出应用从不支持的方法和 Header。配置应与实际客户端协议一致。

7. 全局、Router 和单路由

全局:

app.use(cors(corsOptions));
app.use("/api", apiRouter);

只对一个 Router:

app.use("/public-api", cors(publicCorsOptions), publicRouter);

单一路由:

app.get("/download/:id", cors(downloadCorsOptions), downloadHandler);

如果绝大多数接口策略相同,优先全局配置,避免某个错误路由漏掉 Header。特殊公开接口再局部覆盖。

8. OPTIONS 的处理

app.use(cors(...)) 位于路由之前时,应用级中间件通常已经能处理预检。局部路由场景可显式添加:

app.options("/api/{*splat}", cors(corsOptions));

Express 5 使用较新的路径匹配语法;命名通配符比旧教程里的裸 * 更稳妥。是否需要显式 app.options 应结合当前 cors 版本和挂载位置验证。

9. 把拒绝 Origin 交给错误处理器

app.use(cors(corsOptions));
app.use(express.json());
app.use("/api", apiRouter);

app.use((error: unknown, req, res, next) => {
  if (error instanceof Error && error.message.startsWith("不允许的 Origin")) {
    res.status(403).json({ code: "CORS_ORIGIN_DENIED", message: error.message });
    return;
  }

  next(error);
});

注意:被拒绝的跨源浏览器页面通常仍无法读取这个 JSON,这是预期行为;JSON 主要供服务端日志和非浏览器调试使用。

10. 验证配置

正式请求:

curl -i http://127.0.0.1:8080/api/users \
  -H "Origin: http://localhost:5173"

预检:

curl -i -X OPTIONS http://127.0.0.1:8080/api/users \
  -H "Origin: http://localhost:5173" \
  -H "Access-Control-Request-Method: POST" \
  -H "Access-Control-Request-Headers: authorization,content-type"

至少分别测试允许 Origin、拒绝 Origin、无 Origin、OPTIONS、认证失败和业务错误响应。