实战二:生产环境 Nginx + Express

生产环境首先要判断前后端在浏览器看来是否同源,再决定是否需要 CORS。

1. 同域名路径代理:通常不需要 CORS

https://example.com/          → Nginx 返回前端静态文件
https://example.com/api/      → Nginx 反向代理 Express

协议、主机、端口完全相同,fetch("/api/users") 是同源请求。此时一般不应仅因为前后端是两个进程就启用 CORS。

server {
    listen 443 ssl;
    server_name example.com;

    root /var/www/example/dist;
    index index.html;

    location / {
        try_files $uri $uri/ /index.html;
    }

    location /api/ {
        proxy_pass http://127.0.0.1:8080;
        proxy_http_version 1.1;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Request-Id $request_id;
    }
}

注意 proxy_pass 尾部斜杠会影响转发路径,部署时应通过 Express 日志确认收到的是 /api/users 还是 /users

2. 跨子域部署:需要 CORS

https://www.example.com → 页面
https://api.example.com → API

二者主机不同,属于跨源。建议由 Express 集中生成 API 的 CORS Header:

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

const app = express();

const corsOptions: CorsOptions = {
  origin: ["https://www.example.com"],
  methods: ["GET", "POST", "PUT", "PATCH", "DELETE"],
  allowedHeaders: ["Content-Type", "Authorization"],
  exposedHeaders: ["Content-Disposition", "X-Request-Id"],
  maxAge: 600,
};

app.set("trust proxy", 1);
app.use(helmet());
app.use(cors(corsOptions));
app.use(express.json());

Bearer Token 不要求 credentials: true。若认证使用跨源 Cookie,则改用精确 Origin 并启用 credentials: true,同时正确设置 Cookie:

res.cookie("session", token, {
  httpOnly: true,
  secure: true,
  sameSite: "none",
  path: "/",
});

SameSite=None 通常要求 Secure,所以生产环境必须使用 HTTPS。Cookie 登录还需要考虑 CSRF,CORS 不能代替 CSRF 防护。

3. 页面 Header 与 API Header 分工

Nginx 返回的 index.html 才是页面文档,因此控制页面加载脚本、样式和连接目标的 CSP,应设置在 HTML 响应上。Express 给 JSON 加 CSP,通常不能保护早已由 Nginx 返回的页面。

推荐初始职责:

内容 推荐负责人
HTML 的 CSP、frame 限制 前端站点所在的 Nginx
HSTS TLS 最外层 Nginx
API CORS Express
API nosniff 等安全 Header Express Helmet
静态资源缓存 Nginx
文件接口的 Content-Disposition Express

不要在 Nginx 和 Express 同时维护两套 CORS 白名单。两个不同的 Access-Control-Allow-Origin 值不会“取并集”,浏览器反而会拒绝响应。

4. Nginx Header 注意事项

add_header Strict-Transport-Security "max-age=31536000" always;

always 表示对更多状态码也添加 Header。HSTS 只应在 HTTPS 已稳定工作后启用;includeSubDomains 会影响所有子域,不能随意添加。

若 Nginx 与上游都设置同名 Header,应明确移除或保留哪一层,而不是碰运气:

# 仅在确定由 Nginx 接管该 Header 时使用
proxy_hide_header Access-Control-Allow-Origin;

5. 验证生产链路

查看页面 Header:

curl -I https://www.example.com/

查看 API Header 和响应体:

curl -i https://api.example.com/api/users \
  -H "Origin: https://www.example.com"

模拟预检:

curl -i -X OPTIONS https://api.example.com/api/users \
  -H "Origin: https://www.example.com" \
  -H "Access-Control-Request-Method: POST" \
  -H "Access-Control-Request-Headers: Content-Type, Authorization"

同时绕过 Nginx 请求本机 Express,可以定位问题处于代理层还是应用层:

curl -i http://127.0.0.1:8080/api/users \
  -H "Origin: https://www.example.com"

如果本机正常而公网异常,重点检查 Nginx 配置是否已 reload、代理 location、Header 覆盖和缓存。如果两处都异常,重点检查 Express 中间件顺序与环境变量。

6. 上线策略