☰
Nginx安全头配置指南:从CSP到HSTS筑牢Web防线
2026/10/14 3:49:40 网站建设 项目流程

1. 为什么要在 Nginx 层配置安全头

做 Web 开发或者运维的,多多少少都听说过“响应头少了几个字段被安全审计打了回票”这种事。安全头说白了就是服务器在 HTTP 响应里附加的一组头部字段,告诉浏览器“这个页面该怎么管自己”。浏览器拿到这些指令之后,会主动帮你拦截掉一部分跨站脚本、点击劫持、MIME 类型嗅探这类常见攻击。

那为什么非要在 Nginx 这一层加,而不是在应用框架里加?两个原因:一是省事,Nginx 统一管控,所有后端应用不管你是 PHP、Java、Node 还是 Python,只要走了这一层代理,头部全部补齐,不用每个服务单独写一遍;二是稳定,安全头是应用入口处的一道闸门,就算后端代码有历史包袱改不动,也能在接入层把底线守住。

我见过不少团队,框架里写了安全头的中间件,结果某个老接口绕过了中间件直接返回响应,安全头就丢了。放到 Nginx 上,只要配置不出错,所有经过代理的响应都会带上,除非后端主动覆盖。这个覆盖面就是 Nginx 层的核心价值。

适合谁来参考?两类人:一类是刚接手站点安全改造的运维或全栈开发,需要快速给线上站点补齐安全配置;另一类是写后端但对 HTTP 细节不太熟的开发者,理解这些头的作用之后,能在接口设计阶段就把安全因素考虑进去,省得后面返工。

需要说明的是,Nginx 安全头不是“配了就绝对安全”,它防的是浏览器侧的常见攻击路径,服务端自身的漏洞、业务逻辑问题该修还得修。这个定位先摆清楚,后面做配置的时候才不会产生“我全部配了怎么还被通报”的误解。

2. 常用安全头逐一拆解

2.1 X-Frame-Options:防点击劫持

点击劫持是攻击者用透明的 iframe 把你的页面嵌到恶意网站上,用户以为在点别人页面的按钮,实际上点的是你的页面。X-Frame-Options 就是告诉浏览器“我这个页面允不允许被 iframe 嵌入”。

配置方式:

add_header X-Frame-Options "SAMEORIGIN" always;

三个取值:DENY 表示完全禁止被嵌入,SAMEORIGIN 表示同源页面可以嵌,ALLOW-FROM 指定某个域名可嵌。大多数场景 SAMEORIGIN 就够了,比如你自己的站点页面里用 iframe 嵌自己站内的内容。用 DENY 的话,连自己站内 embed 都会被拦,有时候反而误伤功能。

实际配置的时候有个注意点:如果用了 Content-Security-Policy 的 frame-ancestors 指令,它和 X-Frame-Options 会同时生效,浏览器会取更严格的那个。CSP 的 frame-ancestors 能支持多个域名白名单,比 X-Frame-Options 灵活,但兼容旧浏览器还是得靠 X-Frame-Options 兜底。

2.2 X-Content-Type-Options:禁止 MIME 类型嗅探

这是我最喜欢的一个头,因为配置无脑、收益直接。老版本浏览器在响应里没写 Content-Type 或者写得比较含糊的时候,会自己去“猜”文件类型,比如一个文本文件里嵌了 HTML 标签,浏览器可能把它当网页渲染,这就是 MIME 嗅探。如果这个文件可以被上传(比如论坛头像),攻击者就能构造一个伪装成图片的 HTML 文件,诱导浏览器执行脚本。

配置方式:

add_header X-Content-Type-Options "nosniff" always;

加了 nosniff 之后,浏览器严格按 Content-Type 处理响应,CSS 必须以 text/css 返回、JS 必须以 JavaScript MIME 类型返回,否则直接拒绝执行。这一个头能把上传绕过类的 XSS 挡掉一大半,而且没有任何副作用,属于必配项。

2.3 Referrer-Policy:控制 Referrer 信息泄露

这个头的实际影响被很多人低估了。页面跳转、加载外部资源的时候,浏览器会自动带上 Referrer 头告诉对方“我从哪来”,这个字段里往往带着 URL 参数。如果 URL 参数里有 token、session id、用户标识这类信息,就等于把敏感数据泄露给了第三方站点。

我处理过一个线上事故:站点的分享链接里带了用户邀请码,用户点击跳转到外部落地页,Referrer 把完整带参 URL 传了出去。后面在 Nginx 上加了 Referrer-Policy 才控制住。

推荐配置:

add_header Referrer-Policy "strict-origin-when-cross-origin" always;

这个取值的意思:同源请求发送完整 URL,跨源请求只发送 origin(协议+域名+端口),HTTPS 降级到 HTTP 时不发送。这是目前浏览器默认行为,也是安全性和功能之间的最佳平衡。如果你后端需要拿完整来源做统计,可以酌情调整,但原则上不要用 unsafe-url。

2.4 Permissions-Policy:限制浏览器特性的启用

这是个较新的头,作用是告诉浏览器页面能用哪些功能特性,比如摄像头、麦克风、地理位置、全屏、弹窗等。即使攻击者注入了脚本,没有权限也调不动这些敏感接口,相当于给浏览器的功能权限加了一道锁。

配置方式:

add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;

括号为空表示完全禁止,也可以写成 self 表示仅同源可用,或者指定可信域名。实际操作中建议按业务需求来,别一刀切全禁了,否则后面接入视频通话、地图定位功能的时候还得回来改配置。

3. 两个重点头:CSP 与 HSTS

3.1 CSP:内容安全策略的配置思路

Content-Security-Policy 是最有分量但也是最有坑的安全头。它告诉浏览器页面允许加载哪些来源的资源,包括脚本、样式、图片、字体、连接、frame 等等。配置得当的话,XSS 注入的脚本根本执行不了,因为来源不在白名单里。

一个常见的基础配置:

add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self' data:; connect-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'self'" always;

拆开看:

  • default-src 'self':所有资源默认只能从同源加载,这是兜底规则。
  • script-src:脚本来源,'self' + 内联 + eval。注意了,'unsafe-inline' 和 'unsafe-eval' 是放宽项,如果你的站点没有用内联脚本,尽量去掉。
  • style-src:样式来源。很多前端框架依赖内联样式,先留着 'unsafe-inline' 跑通再说。
  • img-src data::允许 data URI 图片,这是给 base64 小图标留的口子。
  • object-src 'none':禁止 Flash、Java Applet 这类老古董插件加载。
  • frame-ancestors 'self':只允许同源 iframe 嵌入,作用等同于 X-Frame-Options。

配置 CSP 最容易出的问题是页面功能大面积崩溃。脚本被拦、样式错乱、接口连不上,排查起来头疼。我的建议是分三步走:

第一步,先用 Content-Security-Policy-Report-Only 模式上线,只上报不拦截:

add_header Content-Security-Policy-Report-Only "default-src 'self'; script-src 'self'";

浏览器遇到违规会向后端指定的地址发报告,但不会真的拦资源。跑一两周,看上报日志里被拦的是什么资源,在白名单里逐步补。

第二步,把白名单补全,确认没有非法资源后再切强制模式。第三步,上线后持续关注浏览器控制台报错,因为前端代码更新可能引入新的第三方资源,CSP 需要跟着维护。

还有一个常见误区:不是 CSP 里写了 frame-ancestors 就能完全替代 X-Frame-Options。CSP 对老版本浏览器的覆盖不如 X-Frame-Options 全面,两个都要配。

3.2 HSTS:强制 HTTPS 访问

HTTP Strict-Transport-Security 的作用是告诉浏览器:这个域名只能走 HTTPS,不要用 HTTP 访问。配置之后浏览器会在本地记住这个规则,之后用户手动输入 http:// 开头地址,浏览器也会自动改成 HTTPS,从源头杜绝中间人劫持。

配置方式:

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

参数含义:max-age 是记住秒数,31536000 正好一年;includeSubDomains 表示子域名也强制 HTTPS;preload 是申请加入浏览器内置的 HSTS 预加载列表。

这个头的坑比较深,我来逐个说。

第一个坑:依赖 HTTPS。HSTS 只在 HTTPS 响应里生效,如果用户在第一次访问时走的是 HTTP,服务器没法下发 HSTS 头,中间人照样可以把请求劫持到恶意站点。这也是 HSTS Preload 列表存在的意义——浏览器出厂时就内置了哪些域名强制 HTTPS,不走第一次访问流程。

第二个坑:includeSubDomains 要谨慎。加了这条之后,某个子域名如果没上 HTTPS 就要出大事。线上存在裸 HTTP 的子域名,比如内部管理系统、图片服务器没配证书,浏览器也会强制用 HTTPS 访问,直接打不开。

第三个坑:preload 一旦生效很难撤回。你把域名提交到预加载列表之后,想撤销的话要等一个很长的周期,而且各浏览器策略有别。提交之前务必确认全站 HTTPS 已经完备。

第四个坑是实际运营中容易忽视的:临时证书或测试环境的 HTTPS 也会下发 HSTS。一旦 max-age 太长,浏览器记住了,你后面换回 HTTP 调试,浏览器照样强制跳 HTTPS。调试的时候可以把 max-age 设小一点,比如 3600,或者只在生产环境配置这个头。

3.3 X-XSS-Protection 的取舍

这里单独说一下 X-XSS-Protection,因为这个头比较特殊。它是老版本浏览器内置的 XSS 过滤器,配置方式:

add_header X-XSS-Protection "1; mode=block" always;

但是这个过滤器本身存在被攻击者绕过的历史,而且有时候会误伤正常页面,所以新版浏览器已经逐渐移除了这项功能。当前主流观点是:配置了 CSP 之后,X-XSS-Protection 的意义已经不大,甚至在 Chrome 里已经被弃用。

我的做法是:如果是新旧兼容性要求高的项目,配上无妨,mode=block 顶多就是把页面拦截掉,不会造成功能异常;如果站点已经上了完善的 CSP,这个头可以不配或者保持默认,不必过度纠结。

4. 完整配置示例与常见误区

4.1 一份可复用的安全头配置模板

下面给出一份可直接抄的配置,放在 server 块里:

server { listen 443 ssl http2; server_name example.com; # 核心安全头 add_header X-Frame-Options "SAMEORIGIN" always; add_header X-Content-Type-Options "nosniff" always; add_header Referrer-Policy "strict-origin-when-cross-origin" always; add_header Permissions-Policy "camera=(), microphone=(), geolocation=(), payment=()" always; add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; # CSP 按需调整 add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self' data:; connect-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'self'" always; # 反向代理配置 location / { proxy_pass http://backend; proxy_set_header Host $host; 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; } }

有几个配置上的细节我要强调一下。

第一个细节,add_header 后面的 always 参数别漏。默认情况下 Nginx 只在响应码为 200、201、204、206、301、302、303、304、307、308 时附加 add_header 定义的头部,如果响应是 404、500 这类错误码,头部就不会加上。加了 always 之后,所有响应码都会带上。安全审计的时候最容易抓到的就是没加 always 导致 404 页面没有安全头。

第二个细节,关于 add_header 继承。这里有个老坑:如果在 server 块定义了 add_header,而某一个 location 块里也写了 add_header,那么 server 块里的全部 add_header 都会失效,Nginx 不会做合并,location 里的会整体覆盖。解决方法是要么所有 location 都不写 add_header,要么把公共的安全头放到一个独立配置文件里,在每个 location 里用 include 引进来。

比如先建一个 /etc/nginx/security-headers.conf:

add_header X-Frame-Options "SAMEORIGIN" always; add_header X-Content-Type-Options "nosniff" always; add_header Referrer-Policy "strict-origin-when-cross-origin" always; add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; add_header Content-Security-Policy "default-src 'self'" always;

然后在 server 块和 location 块里分别引入:

server { include /etc/nginx/security-headers.conf; location / { # 这里如果写了 add_header,务必也 include 一次 include /etc/nginx/security-headers.conf; proxy_pass http://backend; } }

第三个细节,X-Forwarded-Proto 的配置。如果你的站点是 HTTPS 对外,但 Nginx 和后端之间是 HTTP,后端应用判断请求是不是 HTTPS 就靠这个头。不配的话,有些框架会自动生成 HTTP 链接,导致页面里混入 http 资源,浏览器控制台一片 warning,HSTS 也保护不了这些混入的请求。

4.2 验证配置是否生效

配置完成之后,先测试 Nginx 配置有没有语法错误:

nginx -t

然后重载:

nginx -s reload

验证响应头是否生效,可以用 curl:

curl -I https://example.com

重点看返回的响应里有没有这几个头:

curl -I https://example.com | grep -i "strict-transport-security\|content-security-policy\|x-frame-options\|x-content-type-options"

也可以用在线工具检测,比如安全头检测类的服务,会逐项列出评分和缺失项。这类工具测完一般会给个等级,常见的 A+ 就是 HSTS 配置正确且 CSP 足够严格。

需要特别提醒的是,如果在前端页面部署了 Service Worker 或者 CDN 缓存,响应头可能被 CDN 节点缓存下来,源站改了配置但 CDN 还是旧头。这种情况要在 CDN 侧刷新缓存或者修改 CDN 的回源 Header 设置。

4.3 配置顺序与性能考量

安全头全部加在 Nginx 层,对性能的影响几乎可以忽略不计。add_header 只是往响应里追加文本,每个响应头加起来不过几百字节,在带宽面前毫无存在感。真正需要注意的是如果站点有大量小文件请求,比如验证码图片、静态小图标,每个响应都带上 CSP 这种长头部,响应体积会变大 5% 到 10% 左右。这不是什么大问题,但如果你的业务对流量成本极其敏感,可以区分静态资源路径和动态页面路径,静态资源只配 nosniff 和 X-Frame-Options,动态接口再配完整 CSP。

5. 常见问题与排查技巧

5.1 问题速查表

现象可能原因排查方向与方法
响应头没有出现add_header 没加 always,或者 location 块覆盖了 server 块检查 location 里是否写了其他 add_header,用 nginx -T 查看实际生效配置
CSP 上线后页面样式全乱内联样式被拦截,或者加载了第三方 CDN 资源浏览器控制台看具体被拦的资源,将其域名或 'unsafe-inline' 加入白名单
整站打不开,提示重定向过多HSTS + 后端强制跳转 HTTPS 形成循环检查后端是否有 HTTP 到 HTTPS 的跳转逻辑,Nginx 层做了跳转后端又跳一次
子域名突然全部无法访问includeSubDomains 配置但子域名没有 HTTPS要么给子域名补证书,要么去掉 includeSubDomains
前端埋点上报失败connect-src 没放开对应的接口域名在 CSP 的 connect-src 里加上埋点服务域名
WebSocket 连接失败connect-src 没有允许 ws/wss 协议添加 connect-src wss:// 域名,注意不是写进 script-src
老浏览器 iframe 嵌入被拦只配了 CSP frame-ancestors,没配 X-Frame-Options两个头同时配置,老浏览器认 X-Frame-Options
404 页面安全头缺失add_header 缺省不带错误码响应统一加上 always 参数
检测工具提示缺少 CSP frame-ancestorsCSP 配置里没有写 frame-ancestors在 CSP 里补充 frame-ancestors 'self' 或指定域名
页面加载变慢CSP 白名单过多或报告模式上报频繁关闭 Report-Only,减少多余域名,静态资源合并

5.2 容易被忽视的细节

Nginx 变量和 add_header 一起用的时候要小心转义。比如动态设置 CSP 的某个值,需要在配置里写清楚变量名,变量值里如果含有引号或分号,Nginx 编译配置可能会出问题。安全头的值尽量写死,不要拼接用户输入,否则等于给自己开了一个配置注入的口子。

还有一个运营上容易踩的坑:日志里出现大量 CSP 违规上报,很多人第一反应是白名单没写全,其实有可能是浏览器插件在页面里注入了脚本。这类插件注入的资源不属于站点本身,CSP 会拦,但自己把插件域名加进白名单又适得其反,反而削弱了策略强度。这种情况下要分析上报来源,区分业务资源和插件注入。

5.3 上线前的检查清单

结合我自己的上线经验,列一份执行顺序:

  • 先确认全站 HTTPS 完备,包括所有子域名和裸域,再考虑上 HSTS 带 includeSubDomains。
  • 先在预发环境完整配置所有安全头,用 curl 逐个核对响应头是否存在、取值是否正确。
  • 浏览器控制台逐一检查 Console 报错和 Network 里被拦截的资源。
  • 检查后端应用自身是否可能产生覆盖安全头的逻辑,比如某些框架会自动输出 Vary 头或者自定义安全头。
  • 配置发布之后观察线上错误日志,特别是 CSP 上报接口有没有异常波动。
  • 留存配置变更记录,安全头后续需要迭代时方便比对。

6. 实操过程与配置迭代经验

6.1 一个动态接口场景下的安全头处理

真实项目里,Nginx 不一定只代理静态页面,可能还挂着一堆 API 接口。接口响应一般没有 HTML 页面那些点击劫持的问题,但接口会返回 JSON 数据,如果响应头没设置 nosniff,老浏览器有可能误把 JSON 当 HTML 解析,MIME 嗅探就会生效,数据就有被脚本执行的风险。我的习惯是给 API 路径单独配置精简版安全头:

location /api/ { add_header X-Content-Type-Options "nosniff" always; add_header X-Frame-Options "SAMEORIGIN" always; add_header Content-Type "application/json; charset=utf-8" always; proxy_pass http://backend_api; }

设置 Content-Type 不是 Nginx 安全头的标准做法,但它能进一步防止浏览器猜测响应类型。这类细节在接口改动频繁的项目里尤其有用。

6.2 多域名共用一个 Nginx 时的配置组织

一个 Nginx 实例挂了十几个站点,每个站点的 CSP 策略都不一样,直接在每个 server 块里复制粘贴会带来维护噩梦。我的做法是给每个站点建一个独立的配置文件目录,比如 /etc/nginx/conf.d/security/ 下放站点名.conf,里面写该站的安全头,然后主配置里统一 include:

include /etc/nginx/conf.d/security/*.conf;

这样每个站点一个文件,集中管理又互不干扰,改动某个站点的策略时不会影响其他站点。

还有一种做法是用 map 指令根据 server_name 动态生成安全头值,不过这个方式对配置语法要求更高,容易把自己绕晕,小规模站点不推荐。

6.3 配置回滚预案

安全头配置上线后如果出现大面积故障,回滚比修复更快。nginx -s reload 是平滑重载,不影响现有连接,所以回滚只需要把配置改回去再 reload 一次。但有一点要注意:HSTS 一旦被浏览器记住,即使你回滚配置,浏览器端依然强制 HTTPS,这个“记忆”不会随配置回滚而消失。所以 HSTS 的 max-age 一开始不要设置太长,确认稳定运行之后再逐步调大,是一个比较稳妥的上线节奏。

6.4 移动端场景下的 Referrer-Policy 适配

有段时间我在做 H5 页面分享功能,发现从微信内打开的页面跳转外部落地页时,Referrer 字段的格式和桌面浏览器不一样。排查后发现是微信内置浏览器对 Referrer-Policy 的支持不完全符合标准,strict-origin-when-cross-origin 在某些版本下被当成了旧值处理。处理方式是先确认目标浏览器是否支持新取值,再决定要不要降级到 no-referrer-when-downgrade。安全头配置不能只在 Chrome 里验证,移动端 WebView 的响应行为也要列入测试范围。

7. 安全头不是终点

配置完这一套安全头,站点的安全基线算是立住了。但我想多说一句,安全头防护的是浏览器侧的常见攻击路径,服务端本身的漏洞、组件漏洞、业务逻辑越权这些问题,安全头一概管不了。实际做安全建设的时候,安全头是性价比最高的起点,不是全部。

我个人踩过不少坑,最大的体会是两点:第一,任何安全头配置上线前都要在真实浏览器环境里验证效果,特别是 CSP,别盲目追求严格,页面功能崩了影响业务,再严格的策略也留不住;第二,安全头配置是一个持续维护的过程,前端引了新的第三方库、加了新的外部域名,都要同步更新策略,一年不动的配置基本等于失效。

从 Nginx 这一层入手补安全头,确实是投入产出比很高的做法。碰到过不少项目,在代码层面改安全逻辑要排期、要测试,而 Nginx 配置只需要几分钟就能补上,效果来得快。先动手把基础安全头补齐,再逐步完善 CSP 和 HSTS 的细节,这个路线走下来,站点的防护能力会有明显提升。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询