实战二:生产环境 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. 上线策略
- 用预发布域名模拟真实 HTTPS、子域和 Nginx。
- 正常响应之外,还测试 OPTIONS、401、403、404 和 500。
- CSP 先采用 Report-Only,收集实际依赖后再强制执行。
- CORS 白名单来自经过校验的环境变量,不使用包含判断或宽泛正则。
- 发布后分别用真实浏览器和 curl 验证。
- Header 配置变更应可独立回滚。