前后端分离架构中 Helmet 到底影响谁

考虑最常见的生产结构:

浏览器
  ↓ HTTPS
Nginx
  ├─ GET /index.html      → 返回前端 HTML
  ├─ GET /assets/app.js   → 返回前端静态文件
  └─ GET /api/users       → 代理到 Express

关键原则是:响应头属于具体响应。谁生成或最终修改这份响应,谁就决定浏览器在处理这份内容时看到什么策略。

1. Express 的 Header 不会自动保护 Nginx 页面

假设只有 Express 使用:

app.use(helmet());

那么 /api/users 可能带 CSP、HSTS、CORP 等 Header,但 /index.html/assets/app.js 是 Nginx 直接返回的,不会经过 Express,因此不会自动获得这些 Header。

最容易误解的是 CSP:

index.html 响应上的 CSP → 控制这个页面加载哪些脚本、样式、图片和 API
JSON API 响应上的 CSP  → 不会反向控制已经打开的 index.html

因此,真正保护前端页面的 CSP 通常应由提供 HTML 的 Nginx、CDN 或前端托管服务设置。

2. API 响应上的 Header 仍可能有影响

不能由此得出“API 不需要 Helmet”。Express API 可能返回多种内容:

nosniff、CORP、下载相关 Header 和正确 MIME 类型会影响这些响应。API 若返回可直接打开的 HTML,CSP、frame 和 opener 策略也会直接影响该文档。

最佳实践是按响应内容和用途配置,而不是仅按“前端服务/后端服务”的名称判断。

3. 同域路径代理通常不需要 CORS

如果浏览器访问:

https://www.example.com/
https://www.example.com/api/users

协议、主机名和端口都相同,浏览器认为同源。即使 Nginx 在内部把 /api 转发到 127.0.0.1:8080,内部代理地址也不会改变浏览器看到的 Origin,通常无需 CORS。

如果是:

https://www.example.com/
https://api.example.com/users

主机名不同,属于跨源,需要 API 端正确返回 CORS Header。两个域名属于同一家公司或可能是 same-site,并不能免除 CORS。

4. 页面 CSP 与 API CORS需要同时允许

跨子域调用成功需要至少经过两类检查:

www.example.com 的 HTML CSP
  connect-src 是否允许 https://api.example.com
                    ↓
api.example.com 的 CORS
  是否允许 Origin: https://www.example.com

任何一层拒绝,前端 fetch 都可能失败。CSP 错误通常在 Console 中明确提到 connect-src,CORS 错误则会提到 Access-Control-Allow-Origin 等字段。

5. 推荐的职责划分

内容/策略 推荐责任方 原因
HTML 的 CSP 提供 HTML 的 Nginx/前端托管 它最了解构建资源来源
静态资源缓存和 MIME Nginx 它直接提供资源
HSTS 最外层 HTTPS Nginx 统一覆盖公网响应
API CORS Express 可结合路由、环境白名单和认证方式
API JSON 的 nosniff Express 或统一 Nginx 保证最终响应存在且不冲突
API 文件的 CORP Express 它了解文件是否允许跨源使用
iframe 策略 返回 HTML 的服务 只有文档提供方知道嵌入需求

这不是绝对规定。大型平台可能由边缘网关统一治理,关键是保持单一、可追踪的责任来源

6. Nginx 与 Express 重复设置的风险

假设 Express 返回:

Access-Control-Allow-Origin: https://www.example.com

Nginx 又添加:

Access-Control-Allow-Origin: *

最终出现两个值,浏览器很可能拒绝响应。安全 Header 也可能外层覆盖内层,使本机 curl 8080 正常而公网域名失败。

排查时同时比较:

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

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

两份结果的差异通常来自 Nginx、CDN、HTTPS 或不同路由配置。

7. Markdown HTML 预览场景

当前工程存在 Markdown ZIP 转 HTML 的业务,可以把它分为两种响应:

下载 ZIP

附件下载通常不需要像页面一样加载脚本。重点是:

在线预览 HTML

用户上传或转换出的 HTML 属于不可信内容。仅靠 Helmet 默认值不够,应该优先考虑:

若业务要求运行用户脚本,风险模型会完全改变,应使用强隔离方案,而不是简单给 CSP 增加 'unsafe-inline'

8. 如何防止上线才出问题

8.1 让预发布环境接近生产

尽量具备:

8.2 检查最终公网响应

不能只访问 Express 的 8080 端口。至少检查页面、API 成功/失败、文件、OPTIONS:

curl -i https://www.example.com/
curl -i https://www.example.com/api/users
curl -i -X OPTIONS https://www.example.com/api/users \
  -H "Origin: https://frontend.example.net" \
  -H "Access-Control-Request-Method: POST"

8.3 使用真实浏览器回归

curl 不执行 CSP、CORP 和 CORS,因此最终还要测试:

8.4 逐步收紧

CSP 可以先用 Report-Only 收集违规,再逐步阻断。上线时应具备配置回滚手段,但不要把“临时关闭所有安全头”当作长期修复。

9. 一个决策流程

碰到某个 Header 是否需要调整时,依次问:

  1. 它出现在哪个响应:HTML、JSON、图片还是下载?
  2. 最终是谁返回:Nginx、Express、CDN 还是对象存储?
  3. 浏览器用什么方式消费:文档、fetch、iframe 还是资源标签?
  4. 是同源、same-site 还是完全跨站?
  5. 放宽后会增加什么风险,能否只针对一个路由或来源调整?

回答完这些问题,配置通常会比“全局关闭 Helmet”或“CORS 全部设为 *”更可靠。