安全响应头
这一课你将能做到
- 解释 HSTS、
X-Content-Type-Options、Referrer-Policy、CSP frame-ancestors 在响应边界的作用
- 对照
prod.conf.example,说出 www 与 admin 在点击劫持防护上的差异
- 正确使用
add_header … always,并说明 X-Forwarded-* 与真实 IP(含 CDN 时)如何配合
为什么要学
TLS 解决「传输被窃听」;响应头解决「浏览器怎么对待这份内容」:能否被 iframe、是否强制以后走 HTTPS、是否禁止 MIME 嗅探。
这些头放在 Nginx 统一下发,比每个上游应用各自遗忘更稳。CodeRoadMap 主站与 admin 策略不同:后台更严。
同时,转发头决定 Java 限流与审计看见的「真实客户端」——头设错,安全与风控一起歪。
先建立图像
上游响应(Spring / Next / 静态文件)
│
▼
Nginx 追加安全头 ──► 浏览器执行策略
│
└─ 另:请求方向补 X-Forwarded-* ──► 上游记 IP / 协议
| 头 | 人话 |
|---|
| HSTS | 「以后请只用 HTTPS 来找我」 |
| nosniff | 「别把文本猜成脚本」 |
| CSP frame-ancestors / X-Frame-Options | 「谁可以(或不可以)把我嵌进 iframe」 |
| Referrer-Policy | 「跳出去时带多少来源信息」 |
跟着做
步骤 1:读主站 add_header
deploy/nginx/prod.conf.example www server:
# always:4xx/5xx 也带上,避免错误页丢 HSTS
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header X-Content-Type-Options "nosniff" always;
add_header Referrer-Policy "no-referrer-when-downgrade" always;
add_header Content-Security-Policy "frame-ancestors 'self'" always;
笔记:
- HSTS 一年(需站点 HTTPS 已稳定)
- CSP 此处先管 嵌入策略(frame-ancestors),不是完整脚本 CSP 百科
步骤 2:对比 admin 更严策略
add_header X-Frame-Options "DENY" always;
add_header Content-Security-Policy "frame-ancestors 'none'" always;
管理后台:禁止任何站点嵌入,降低点击劫持(用户以为在点后台按钮,实际点到隐藏层)。
主站允许 'self',策略略宽;后台直接封死。
步骤 3:浏览器验证
用已部署环境或本地 HTTPS 实验:
- 打开站点 → DevTools → Network → 文档响应
- 确认存在
strict-transport-security、x-content-type-options 等
- 再看一条 404/502(若可复现)是否 这些头——验证
仍带
always
步骤 4:请求方向的转发头(再钉一次)
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
Spring 限流、登录审计依赖真实 IP;生成 redirect / cookie 依赖 proto=https。
与 sb-security 的转发头信任配置对照阅读。
步骤 5:前面还有 CDN 时
# 示意:只信任 CDN 网段(请换成官方 IP 段)
set_real_ip_from 10.0.0.0/8;
real_ip_header CF-Connecting-IP; # 或 X-Forwarded-For,按 CDN 文档
否则 $remote_addr 只是 CDN,限流会误伤整网。
步骤 6:与应用头的分工
- Nginx:边缘统一、错误页也能带上的基线头
- Next/Spring:业务相关 CSP 脚本策略、CORS(若有独立 API 域)等
避免两边互相覆盖时「只剩一份且更松」——改前先用 curl -I 看最终结果。
步骤 7:用 curl 做一次「边界体检」
curl -sSI https://www.coderoadmap.cn/ | tr -d '\r' | grep -iE 'strict-transport|x-content-type|content-security|referrer-policy'
应能看到本课讨论的基线头。再对一条会 404 的路径重复一次,确认错误响应仍带 always 头。
若某头缺失:先查该 server/location 是否写了 add_header,再查子 location 是否覆盖导致父级头丢失。
把「200 与 4xx 各测一次」写进上线检查清单——安全头验收不能只看首页成功页。
步骤 8:和限流、审计的闭环
真实 IP 不只是日志好看:Spring 侧公开接口限流、异常登录告警都依赖它。
在笔记画一箭头:浏览器 →(可选 CDN)→ Nginx real_ip → X-Forwarded-For → Spring RateLimiter。
任一环信任错对象,都会出现「整网被限」或「攻击者伪造无辜 IP」。本课把头设对,限流课才有正确输入。
讲清楚要点
- 因为安全头是给浏览器的策略,所以在 TLS 边界(Nginx)统一添加最省事——上游遗漏也能兜住基线。
- 因为默认 add_header 在错误响应可能不生效,所以生产加
always——HSTS 不能只在 200 出现。
- 因为后台被嵌入风险更高,所以 admin 用 DENY / frame-ancestors none——主站可略宽。
- 因为 HSTS 会「粘」在浏览器里,所以仅在 HTTPS 稳定、跳转正确后启用长 max-age——测坏了难立刻自清。
- 因为应用看见的是代理连接,所以必须传 X-Forwarded-*——安全与功能同时依赖。
- 因为客户端可伪造 X-Forwarded-For,所以只信任你管控的代理链——CDN 场景用 real_ip。
- 因为 nosniff 成本极低收益明确,所以应默认开启——减少错误 Content-Type 被当脚本执行的面。
你可能会懵的地方
-
只有 CSP frame-ancestors,算不算「上了 CSP」?
算用了 CSP 的一部分能力。完整防 XSS 的 script-src 策略更复杂,常要前端构建配合;本课先把点击劫持与基线头做对。
-
X-Frame-Options 和 CSP 重复吗?
有重叠。老客户端认 XFO;现代更推荐 CSP frame-ancestors。admin 两者都上更稳。
-
为什么 Referrer-Policy 选 no-referrer-when-downgrade?
同站/升级场景保留有用 referrer;向降级 http 时更谨慎。可按隐私要求再收紧。
-
add_header 在 location 继承有坑?
若父级有 add_header、子 location 又写了自己的,可能不继承父级那组。改复杂站点前查官方继承行为,或在同一层写全。
-
HSTS 的 includeSubDomains 会不会误伤?
会把子域也纳入「以后只走 HTTPS」。若某个子域尚未具备有效证书,用户可能暂时打不开。上线前确认 admin/test 等子域 TLS 就绪,或先不用该指令、待全部子域稳妥后再打开。
错法对照
| 错法 | 问题 | 正解 |
|---|
add_header 无 always | 错误页丢 HSTS | 加 always |
| 尚未稳定 HTTPS 就开超长 HSTS | 用户卡在坏配置 | 先稳定 301/证书再加大 max-age |
| admin 允许任意 iframe | 点击劫持面大 | DENY / frame-ancestors none |
| 盲目信任所有 X-Forwarded-For | IP 伪造 | 只信 CDN/本机代理 |
| 只在 Spring 加头、Nginx 错误页无 | 基线不一致 | 边缘统一 + always |
| 子 location 重写 add_header 却漏抄父级 | 部分路径丢 HSTS/CSP | 同层写全或避免局部覆盖 |
| 只验收首页 200 | 错误页无安全头未被发现 | 200 与 4xx 都 curl -I |
自测三问
- 为什么安全头建议带
always?
- www 与 admin 在 frame 相关头上差在哪里?原因是什么?
- 有 CDN 时,为何不能直接把
$remote_addr 当用户 IP?
小结一句话
安全头在 Nginx 统一下发(always);admin 更严防嵌入;X-Forwarded- 让 Java 看见真实客户端与 https。*
官方延伸