1. 为什么Nginx安全头值得专门写一篇做了这么多年Web服务端,我见过太多把Nginx配置当成"转发请求"工具就完事儿的场景。反向代理、负载均衡、静态资源服务都配置得明明白白,但HTTP响应头这块儿,几乎是被遗忘的角落。直到某次给客户做安全审计,对方的安全报告里密密麻麻列了一堆响应头缺失的问题,我才意识到:安全头这东西,配置成本极低,但价值极高。简单说,Nginx安全头就是通过add_header指令往HTTP响应里塞的特定Header。浏览器收到这些Header之后,会自动启用一些安全策略——比如限制页面能不能被嵌套加载、能不能被嗅探类型、往哪些域名发起请求。核心思路是"防御性编程"的延伸:你的业务代码可能永远写不完所有边界情况,但响应头可以从浏览器层面把意外情况拦下来。这篇文章不是给你罗列几十个安全头然后让你全都加上,而是把我实际项目中筛选出来、经过线上验证的高价值安全头,从原理到踩坑一次性讲透。适合部署过Nginx但没系统做过安全加固的开发者,也适合刚接触Web安全、想知道"安全头到底是干嘛的"的入门者。不管你是给公司运维同事写加固文档,还是自己折腾个人项目,这些配置直接抄作业就能用。还得先说清楚一件事:安全头不是万能的,它挡不住应用层漏洞,比如SQL注入或者任意文件读取这类和业务逻辑强相关的问题。它的核心价值是提升攻击者在浏览器端的利用成本,降低你被社工、点错链接、中招恶意脚本的概率。所以下面的所有配置,我默认你是配合其他安全措施一起用的,而不是只加几个响应头就以为万事大吉。2. 八个核心安全头逐个拆解这八个是我在Nginx配置里实际使用、经过多轮测试验证过的高性价比安全头。选它们的逻辑很简单:要么是防护效果明确、配置简单,要么是行业规范里明确要求。我会把每个头的作用机制、推荐配置和为什么这么配讲清楚,最后再给一份可以直接贴进Nginx的完整配置块。2.1 Strict-Transport-Security(HSTS)Strict-Transport-Security,简称HSTS。这个头的作用一句话讲完:告诉浏览器,这个域名只能走HTTPS,别用HTTP。流程是这样的:用户第一次访问你的网站,如果输入的是http://开头的地址,浏览器先发起HTTP请求。服务器在响应里带上Strict-Transport-Security: max-age=31536000; includeSubDomains,浏览器记住这个域名在一年内都强制HTTPS。之后用户再访问,浏览器直接内部跳转HTTPS,根本不会发HTTP请求出去。配置参数推荐:add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;重点说几个参数。max-age单位是秒,31536000就是一年。includeSubDomains意味着所有子域名也继承这个策略。preload是配合HSTS预加载列表用的,加了这个之后,如果哪天你想取消HSTS,就得等max-age过期,想快点儿取消就得手动清浏览器状态,挺麻烦的。所以个人建议:preload可以加,但要想清楚自己是否确定长期只用HTTPS。一个很常见的坑:如果你当前还存在HTTP到HTTPS的跳转服务、或者某个子域名还得走HTTP,比如给旧系统留了口子,那么HSTS这个头就要小心配置。一旦浏览器记住了强制HTTPS,你后面再想绑一个因为证书没配好而只能走HTTP的页面,用户就永远打不开了,报错还会是"无法访问"这种看起来特别吓人的错误。我的做法是:域名全站确定HTTPS之后,先不加includeSubDomains跑一个月,确认子域名都正常,再加完整参数。2.2 X-Content-Type-Options这个头只有一个值:nosniff。作用是禁止浏览器进行MIME类型嗅探。MIME嗅探可能不太好理解,我举个实际例子。你上传了一张图片到服务器,文件确实是PNG格式,但服务端返回的Content-Type写错了。正常情况下,浏览器会去检查文件内容,发现它是图片,就按图片渲染。这个"检查文件内容猜格式"的行为就叫MIME嗅探。听着好像还挺智能,但攻击者可以利用这个特性:把一个恶意脚本文件伪装成图片上传。在旧的浏览器上,如果服务端Content-Type写的是image/png,但文件内容其实是HTML,浏览器可能因为某个标签或内容特点而误判为HTML去执行,进而触发XSS。加上nosniff之后,浏览器严格按Content-Type来决定怎么处理响应内容,不会自己去猜。上传的是image/png就永远按图片处理,是HTML脚本就当附件下载,不会执行。配置就一句:add_header X-Content-Type-Options "nosniff" always;这个头没什么副作用。唯一要注意的是,如果你有某些接口确实存在响应头写错的情况,加上这个头之后,前端可能因为拿到的响应格式不对而报错。这时候不是去掉nosniff,而是去修服务端的响应头。2.3 X-Frame-Options这个头解决的是点击劫持(Clickjacking)问题。点击劫持的攻击方式很阴,攻击者把一个透明的iframe覆盖在你的登录按钮上,用户以为自己在点你的按钮,实际点的是攻击者页面的按钮。这类攻击通过设置X-Frame-Options基本可以根治。可选值有三个:DENY:任何网站都不能用iframe嵌套你的页面。SAMEORIGIN:只有你自己的网站域名可以嵌套加载。ALLOW-FROM uri:允许指定来源嵌套,但这个值在现代浏览器里已经废弃,不用考虑。配置推荐:add_header X-Frame-Options "SAMEORIGIN" always;为什么推荐SAMEORIGIN而不是DENY?因为业务上有可能需要在同域名的页面里嵌套加载自己,比如后台管理系统的预览功能,或者在自身产品生态里需要iframe展示内容。SAMEORIGIN比DENY灵活,防护效果也够用。坑在哪儿呢?现在很多站点用了第三方客服系统、站内内容分享组件、编辑器插件,这些基本都要靠iframe加载你的页面。一加上DENY,客服弹窗直接白屏。这种问题特别隐蔽,前期测试一般发现不了,等业务方反映"页面打不开"你就得排查。所以我的习惯是:业务上线后观察一段时间,确认确实没有iframe嵌套需求,再考虑收紧到DENY。另外说一句,SAMEORIGIN对不同端口是宽容的,只要域名相同,端口不同也可以嵌套。如果你有严格跨域需求,就得配合CSP的frame-ancestors指令来做更细粒度的控制。2.4 Content-Security-Policy(CSP)这个是安全头里的重量级角色。CSP的作用是给浏览器立规矩:你的页面允许从哪里加载脚本、样式、图片、字体、媒体,能不能用eval执行字符串形式的代码,等等。CSP配置的边界很宽,可以用一段字符串就实现复杂的限制策略。先给一个常见的基础配置,适合大多数没有太多第三方资源需求的站点:add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'self'" always;我把每条指令的作用讲透。default-src 'self'是兜底策略,如果后续指令没覆盖到的资源类型,默认只允许同源加载。script-src控制了脚本来源,这个是最容易出问题的。'self'表示同源的脚本可以执行;'unsafe-inline'表示允许内联脚本执行,也就是<script>标签里直接写代码的那种。为什么我不直接禁止unsafe-inline?因为现在很多第三方组件,比如某些验证码、埋点脚本、微信分享SDK都依赖内联脚本。如果你一上来就禁,页面可能白屏或者功能缺一块。稳妥做法是先允许内联,跑通业务再逐步收紧。有个常见的误区得纠正:很多人以为CSP里的script-src能挡XSS的全部可能,但实际上只要你在script-src里写了'unsafe-inline'或'unsafe-eval',XSS防御效果就大打折扣。所以在生产环境,那些能去掉unsafe-inline的场景,尽量去掉。object-src 'none'是禁止<object>标签加载Flash或其他插件资源,这个建议禁掉,没什么正当业务需求会用到。frame-ancestors 'self'其实和X-Frame-Options干的是同一件事,但从策略上更精细。如果你配置了frame-ancestors,它会被现代浏览器优先采纳,X-Frame-Options只是作为旧浏览器兜底。配置CSP最大的风险是误伤业务。这个头一旦配太严,线上会出现各种"神秘问题"——比如某个功能按钮突然无效了,或者图片都裂了,其实都是被CSP拦截了。所以我的建议是:先用Content-Security-Policy-Report-Only模式上线监控一段时间。这个模式下浏览器只上报违规行为、不会真的拦截,你能从report-uri或report-to指令指定的地址收到违规报告,再根据报告逐步收紧策略。2.5 X-XSS-Protection严格来说,X-XSS-Protection在现代浏览器里已经逐步废弃了。因为CSP已经能提供更全面的XSS防御,而且某些浏览器的反射型XSS过滤器反而可能引发问题。但为什么我还是会提到它?因为部分旧版浏览器、部分企业内部环境或依赖旧内核的边缘设备,还认这个头。当作兼容方案可以配置,但别指望它能当主力防护。兼容性配置:add_header X-XSS-Protection "1; mode=block" always;1; mode=block的意思是:启用XSS过滤器,如果检测到反射型XSS攻击,就阻断页面渲染。要注意的是,这个头在某些浏览器版本上会跟CSP冲突,而且如果CSP里允许了unsafe-inline,这个过滤器在某些场景下反而可能把本来安全的页面误判为攻击页面。所以我的处理是:新项目直接不配这个头,靠CSP来做保护;老项目或者对内网环境特别多的情况,才保留这个兼容配置。你如果看到网上有人说一定要配,多半是从2014年左右的文章复制下来的,时代已经变了。2.6 Referrer-Policy这个头控制的是:浏览器在跳转和请求外部资源时,要不要带上来源页面地址——也就是Referer字段。Referer泄露的问题比你想象的大。页面上有一个外部图片,浏览器请求这个图片时自动带上当前页面的完整URL。如果这URL里带了查询参数、用户ID、甚至临时票据,对外就全暴露了。有些行业法规对隐私这方面要求很严,所以Referrer-Policy也算合规层面的一个安全保障。推荐的宽松又有普适性的配置:add_header Referrer-Policy "strict-origin-when-cross-origin" always;这个值是默认的推荐值,含义是:同源请求带上完整URL;跨域请求只带来源域名(不带路径和查询串);从HTTPS跳到HTTP的请求不携带任何Referer。既保证了业务功能正常——比如很多站点的统计系统需要Referer来判断来源——又不会把敏感URL暴露给第三方。如果你做的是一个隐私敏感型产品,比如医疗、支付相关,可以更激进一点,直接用no-referrer,完全不携带来源信息。副作用就是:你可能丢失部分场景下的来源分析数据,某些单点登录流程在旧版本浏览器上也可能受影响。2.7 Permissions-Policy这个头的前身是Feature-Policy,现在规范名已经改成Permissions-Policy。它能控制浏览器的一系列API权限:摄像头、麦克风、地理位置、传感器、全屏、支付等等。现实场景很有用。比如你做的是一个营销页面,完全不需要摄像头和麦克风,那就可以在服务器直接禁掉这些权限,就算页面里的恶意脚本想调摄像头,浏览器直接拒绝,不给任何提示和机会。推荐配置:add_header Permissions-Policy "camera=(), microphone=(), geolocation=(), interest-cohort=(), browsing-topics=()" always;括号里留空表示完全禁止。如果想要只允许当前同源页面使用,而不允许iframe里嵌套的页面滥用,就写成camera=(self),允许同源的iframe使用。我特别解释一下browsing-topics=()和interest-cohort=()。这俩跟广告追踪相关,禁止之后可以在浏览器层面阻断某些隐私追踪机制。如果你做的站点有广告位,可能需要区别于上述配置来调整,否则广告系统可能拿不到投放策略所需的数据。但如果想给用户更好的隐私保护,建议保持禁止状态。配置这个头的时候有个坑:不同浏览器对指令的支持程度不一样。Chrome支持得很好,某些基于Chromium内核的浏览器也还行,但个别浏览器对不认识的指令会直接忽略掉整个响应头。所以你在配置前,最好先在目标浏览器上访问一个/headers类型的测试页面,实际看看各个指令的生效情况再上线。2.8 Cross-Origin-Resource-Policy这个头最容易混淆,但非常管用。它和Access-Control-Allow-Origin最容易搞混。Access-Control-Allow-Origin是配合CORS用的,面向的是跨源资源请求的授权机制;Cross-Origin-Resource-Policy(CORP)则是一个更强的隔离机制,它决定浏览器是否允许其他源加载你的资源。你可以这么理解:CORS是白名单,告诉浏览器"这些来源可以跨域拿我的数据";CORP是黑名单,告诉浏览器"除了我允许的,其他来历不明的请求都不许加载我的资源"。配置推荐:add_header Cross-Origin-Resource-Policy "same-origin" always;same-origin表示只允许同源页面加载资源。如果你的系统还有比较多的跨源图片热链接需求,比如用户上传的图片需要嵌入到小程序或其他业务站点里展示,那就得用cross-origin或者same-site,具体情况具体分析。这个头真正的价值在于防范跨源信息泄露,也就是Spectre这类侧信道攻击时代的遗留防护思路。虽然现在普通项目不一定遇到那么高端的攻击,但加上它成本接近于零,对加固整体安全很有帮助。3. 生产环境配置实操理论说了一大堆,接下来给可以直接用的配置。我把这些安全头整理成一个Nginx片段,分模块讲解,然后教你如何在不影响业务的情况下平滑上线。3.1 完整的安全头配置块这是我在生产环境实际使用过的配置模板,适用于绝大多数Web站点。放在server块里:server { listen 80; listen 443 ssl http2; server_name example.com; # 强制HTTPS访问,HTTP请求直接301跳转 if ($scheme = http) { return 301 https://$host$request_uri; } # 安全响应头集合 add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always; add_header X-Content-Type-Options "nosniff" always; add_header X-Frame-Options "SAMEORIGIN" always; add_header Referrer-Policy "strict-origin-when-cross-origin" always; add_header Permissions-Policy "camera=(), microphone=(), geolocation=(), interest-cohort=(), browsing-topics=()" always; add_header Cross-Origin-Resource-Policy "same-origin" always; add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'self'" always; # 其余业务配置…… }如果你有多个server块,所有需要安全加固的站点都要重复这一段,或者抽取成独立配置文件后用include引入。我的经验是放在conf.d/security-headers.conf文件里,然后各个应用站点的server块里include一份,维护成本会低很多。3.2 不同业务形态的配置差异配置不是一套全打天下,不同类型业务要微调。纯静态站点/内容型网站:资源都来自自己的CDN,没太多第三方脚本,CSP可以做得非常严格,直接去掉unsafe-inline,所有第三方来源都显式白名单化。这类站点配置能收紧到极致,对SEARCH引擎的友好度也不受影响。管理后台类应用:要小心X-Frame-Options和frame-ancestors,如果后台有iframe嵌套编辑器、文件上传预览、富文本组件,要确保CSP的策略允许这些内联资源和iframe来源,否则功能直接崩。我的经验是后台系统的CSP先放宽,稳定运行后再逐步收紧。涉及小程序/公众号开发的服务端:这类场景经常需要被外部平台以iframe方式嵌入,X-Frame-Options不能直接SAMEORIGIN完事,要结合frame-ancestors把对应平台域名放进来。如果只配了SAMEORIGIN,在第三方平台里打开你的落地页就是空白。高流量CDN加速场景:如果你用CDN,安全头通常在源站配一次,然后CDN缓存并下发。但有的CDN配置会主动覆盖源站的响应头。所以上线之后必须用curl -I或者在线Header检测工具确认最终用户实际拿到的是不是你配置的响应头。我之前就吃过这个亏:源站配了HSTS,CDN那边默认给覆盖掉了,导致安全合规检测怎么都不过关。3.3 用curl验证响应头配置完先别急着上线,用curl看一眼响应头对不对:curl -I https://example.com正常的输出会包含这些关键行:HTTP/2 200 strict-transport-security: max-age=31536000; includeSubDomains; preload x-content-type-options: nosniff x-frame-options: SAMEORIGIN referrer-policy: strict-origin-when-cross-origin permissions-policy: camera=(), microphone=(), geolocation=(), interest-cohort=(), browsing-topics=() cross-origin-resource-policy: same-origin content-security-policy: default-src 'self'; script-src 'self' 'unsafe-inline'; ...注意,curl的输出里响应头名的字母大小写不重要,HTTP头本来就是大小写不敏感的。如果你看到某一行缺失,先检查add_header指令是否写错,再看看有没有被其他嵌套的location块覆盖。Nginx的add_header指令在location级别有自己的继承规则,如果你在location里也写了add_header,那么server级别的add_header就全部失效,这一点极其容易被忽视。3.4 分阶段上线策略直接一次性把所有安全头全部上线,风险很大,尤其是CSP和Cross-Origin-Resource-Policy,很容易误伤正在运行的功能。我的做法分三步:第一步,选取一个非核心的环境,比如预发环境,把全部安全头加上,跑一周。期间请业务方尽量完整测试主要功能。第二步,全量上线的时候,先把CSP切到Content-Security-Policy-Report-Only模式,这个模式下浏览器只会往你指定的地址上报违规事件,不会真的拦截任何资源。配置方式是在现有CSP响应头前加Content-Security-Policy-Report-Only,你可以理解成"只记错误日志,不打断业务"。配合report-uri把违规上报收集起来:add_header Content-Security-Policy-Report-Only "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; report-uri /csp-report;" always;其中/csp-report需要你服务端搭建一个简单的接收POST请求的接口,把接收到的数据存到日志或数据库里,我这边是用一个简单的PHP脚本接收并落日志,改造成本很低。第三步,根据上报的违规报告调整策略,确认没有漏网之鱼,再把Report-Only去掉,换成正式生效的Content-Security-Policy。4. 常见问题与排查技巧实录安全头的坑,几乎全集中在"配完之后业务出问题"和"配了但没生效"这两类。下面把我这些年踩过的坑整理成速查表,每一条都对应真实发生过的情况。4.1 配置了安全头但Nginx没生效场景描述:明明在server块里写了add_header,用curl测试却发现对应的响应头没出现。排查思路:先看有没有location块内重复定义add_header。Nginx的变量继承规则是:如果当前location块内声明了任何add_header,那外层的server块add_header就会被全部舍弃。比如:server { add_header X-Frame-Options "SAMEORIGIN" always; location /api/ { add_header X-Request-ID $request_id; proxy_pass http://backend; } }这种情况下,/api/路径的响应里只会出现X-Request-ID,而不会有X-Frame-Options。解决方案有两种:要么把外层的安全头逐个复制到内层location,要么用add_header指令的继承能力提升到http块。我的建议是,把固定的安全头放到http块这个最高层级,因为http块声明的安全头会被所有server和location继承,只有当server或location自己有add_header时才会覆盖。4.2 加了CSP后页面样式全乱场景描述:上线CSP之后,整个页面排版崩掉,但控制台日志里明明显示CSP违规上报。根因分析:style-src没有放行内联样式。很多前端框架、动态主题系统会往<head>里注入<style>标签或直接在元素上写style="..."属性。如果你的style-src默认值过严,这些样式全部被拦下来。我之前用某个后台模板的时候,主题换色功能依赖动态内联样式,配了严格CSP之后换色功能直接失灵,查了半小时日志才反应过来。解决方案:在style-src里加上'unsafe-inline'。如果你实在不想放开内联样式,就得让前端把所有动态样式改成引用外部CSS类,这对很多老项目来说改动太大,得不偿失。4.3 HSTS配置后子域名证书报错场景描述:某天新增了一个子域名,用的证书链没有配完整,在HTTP跳转HTTPS时因为浏览器强制HSTS而直接跳过了HTTP阶段,导致用户看到"连接不是私密连接"的报错页面,而且这个缓存效果还会持续一段时间。排查思路:这就是includeSubDomains的副作用之一。浏览器认了HSTS之后,压根不再发出HTTP请求,你的Nginx跳转逻辑压根走不到,直接死在证书校验环节。所以务必确保所有子域名在启用HSTS之前证书都是有效且完整的。我的习惯:新子域名的证书问题,我都是先在本地hosts里指向预发环境验证,确认证书链没问题之后才切生产DNS。切完之后再通过Chrome的chrome://net-internals/#hsts工具手动查询域名状态,必要时先用delete domain security policies清掉本地缓存再测试。4.4 Permissions-Policy配置导致业务功能异常场景描述:配置之后,某个页面上的"扫一扫"或"定位"功能突然失效。根因分析:之前提到Permissions-Policy会关闭摄像头的camera()权限,但这个配置如果你直接用了默认空白括号,且你的前端页面确实调用了摄像头,那业务就会在毫无提示的情况下失败。因为浏览器不会弹窗询问用户授权,它直接认为权限被策略禁止。解决方案:确认这个页面确实需要摄像头/麦克风权限,把对应的权限指令放宽。比如改成camera=(self),表示只有当前同源页面允许使用摄像头。这样即使用iframe嵌入到其他站点,摄像头也不会被滥用。4.5 Referrer-Policy配置丢失来源数据场景描述:配置了strict-origin-when-cross-origin之后,埋点系统里的来源分析全变成direct或空值。排查思路:这个值相比完全禁止Referer已经温和很多,跨域情况下它至少还会带上域名。所以你丢失的要么是同源页面之间的跳转来源细节,要么是目标站点本身不解析跨域Referer域名。如果业务确实需要收到完整的跳转来源URL,比如分析某个渠道广告的落地页参数,可以考虑用unsafe-url,但强烈不推荐在生产环境这么做,因为URL里很可能带用户标识等参数。4.6 用在线检测工具验证最终链路推荐两个我常用的验证方式:一是浏览器开发者工具的网络面板,拿真实浏览器看响应头;二是用公开的HTTP安全头检测服务,这类服务会模拟主流浏览器发送真实请求,把你响应头里的缺失项和高风险项列出来。值得提醒的是,在线检测服务本身会发起真实请求到你的源站。如果你的源站做了基于UA或IP的拦截,检测结果可能不准。所以最好在预发或测试环境先用curl做一轮基础确认,再上第三方检测工具。4.7 老项目兼容性问题的兜底方案要是你的项目特别老,浏览器环境复杂,没法一步到位直接全部配置,我的兜底方案是先只上三个改动最小的头:X-Content-Type-Options、X-Frame-Options和Referrer-Policy。这三个几乎不会带来什么功能副作用,效果却很明确。等稳定了,再加CSP,最后加HSTS和Permissions-Policy。这个顺序是我在实践中试出来的,能最大程度避免线上事故。个人在实际项目中的体会是:安全头配置的上限不在Nginx,而在浏览器和业务兼容性之间要找平衡。你得先能定位到"哪个资源被哪个头拦了",才能决定是放松策略还是修改业务。如果想降低排查成本,最好在服务端把安全头的版本号打出来,比如在响应头里附带一个自定义字段表明配置版本,这样线上任何一条告警都能快速定位到是不是最近改了安全策略导致的。最后再分享一个小技巧:add_header指令是支持变量的,你可以通过环境变量来区分预发和生产的安全级别。在预发环境暴力配置全禁,在生产环境温和配置,出了事容易对比。上线前用浏览器开发者工具和在线检测扫一遍响应头,再对照业务功能清单过一遍主流程,基本就不会出大问题了。
1. 为什么Nginx安全头值得专门写一篇做了这么多年Web服务端,我见过太多把Nginx配置当成"转发请求"工具就完事儿的场景。反向代理、负载均衡、静态资源服务都配置得明明白白,但HTTP响应头这块儿,几乎是被遗忘的角落。直到某次给客户做安全审计,对方的安全报告里密密麻麻列了一堆响应头缺失的问题,我才意识到:安全头这东西,配置成本极低,但价值极高。简单说,Nginx安全头就是通过add_header指令往HTTP响应里塞的特定Header。浏览器收到这些Header之后,会自动启用一些安全策略——比如限制页面能不能被嵌套加载、能不能被嗅探类型、往哪些域名发起请求。核心思路是"防御性编程"的延伸:你的业务代码可能永远写不完所有边界情况,但响应头可以从浏览器层面把意外情况拦下来。这篇文章不是给你罗列几十个安全头然后让你全都加上,而是把我实际项目中筛选出来、经过线上验证的高价值安全头,从原理到踩坑一次性讲透。适合部署过Nginx但没系统做过安全加固的开发者,也适合刚接触Web安全、想知道"安全头到底是干嘛的"的入门者。不管你是给公司运维同事写加固文档,还是自己折腾个人项目,这些配置直接抄作业就能用。还得先说清楚一件事:安全头不是万能的,它挡不住应用层漏洞,比如SQL注入或者任意文件读取这类和业务逻辑强相关的问题。它的核心价值是提升攻击者在浏览器端的利用成本,降低你被社工、点错链接、中招恶意脚本的概率。所以下面的所有配置,我默认你是配合其他安全措施一起用的,而不是只加几个响应头就以为万事大吉。2. 八个核心安全头逐个拆解这八个是我在Nginx配置里实际使用、经过多轮测试验证过的高性价比安全头。选它们的逻辑很简单:要么是防护效果明确、配置简单,要么是行业规范里明确要求。我会把每个头的作用机制、推荐配置和为什么这么配讲清楚,最后再给一份可以直接贴进Nginx的完整配置块。2.1 Strict-Transport-Security(HSTS)Strict-Transport-Security,简称HSTS。这个头的作用一句话讲完:告诉浏览器,这个域名只能走HTTPS,别用HTTP。流程是这样的:用户第一次访问你的网站,如果输入的是http://开头的地址,浏览器先发起HTTP请求。服务器在响应里带上Strict-Transport-Security: max-age=31536000; includeSubDomains,浏览器记住这个域名在一年内都强制HTTPS。之后用户再访问,浏览器直接内部跳转HTTPS,根本不会发HTTP请求出去。配置参数推荐:add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;重点说几个参数。max-age单位是秒,31536000就是一年。includeSubDomains意味着所有子域名也继承这个策略。preload是配合HSTS预加载列表用的,加了这个之后,如果哪天你想取消HSTS,就得等max-age过期,想快点儿取消就得手动清浏览器状态,挺麻烦的。所以个人建议:preload可以加,但要想清楚自己是否确定长期只用HTTPS。一个很常见的坑:如果你当前还存在HTTP到HTTPS的跳转服务、或者某个子域名还得走HTTP,比如给旧系统留了口子,那么HSTS这个头就要小心配置。一旦浏览器记住了强制HTTPS,你后面再想绑一个因为证书没配好而只能走HTTP的页面,用户就永远打不开了,报错还会是"无法访问"这种看起来特别吓人的错误。我的做法是:域名全站确定HTTPS之后,先不加includeSubDomains跑一个月,确认子域名都正常,再加完整参数。2.2 X-Content-Type-Options这个头只有一个值:nosniff。作用是禁止浏览器进行MIME类型嗅探。MIME嗅探可能不太好理解,我举个实际例子。你上传了一张图片到服务器,文件确实是PNG格式,但服务端返回的Content-Type写错了。正常情况下,浏览器会去检查文件内容,发现它是图片,就按图片渲染。这个"检查文件内容猜格式"的行为就叫MIME嗅探。听着好像还挺智能,但攻击者可以利用这个特性:把一个恶意脚本文件伪装成图片上传。在旧的浏览器上,如果服务端Content-Type写的是image/png,但文件内容其实是HTML,浏览器可能因为某个标签或内容特点而误判为HTML去执行,进而触发XSS。加上nosniff之后,浏览器严格按Content-Type来决定怎么处理响应内容,不会自己去猜。上传的是image/png就永远按图片处理,是HTML脚本就当附件下载,不会执行。配置就一句:add_header X-Content-Type-Options "nosniff" always;这个头没什么副作用。唯一要注意的是,如果你有某些接口确实存在响应头写错的情况,加上这个头之后,前端可能因为拿到的响应格式不对而报错。这时候不是去掉nosniff,而是去修服务端的响应头。2.3 X-Frame-Options这个头解决的是点击劫持(Clickjacking)问题。点击劫持的攻击方式很阴,攻击者把一个透明的iframe覆盖在你的登录按钮上,用户以为自己在点你的按钮,实际点的是攻击者页面的按钮。这类攻击通过设置X-Frame-Options基本可以根治。可选值有三个:DENY:任何网站都不能用iframe嵌套你的页面。SAMEORIGIN:只有你自己的网站域名可以嵌套加载。ALLOW-FROM uri:允许指定来源嵌套,但这个值在现代浏览器里已经废弃,不用考虑。配置推荐:add_header X-Frame-Options "SAMEORIGIN" always;为什么推荐SAMEORIGIN而不是DENY?因为业务上有可能需要在同域名的页面里嵌套加载自己,比如后台管理系统的预览功能,或者在自身产品生态里需要iframe展示内容。SAMEORIGIN比DENY灵活,防护效果也够用。坑在哪儿呢?现在很多站点用了第三方客服系统、站内内容分享组件、编辑器插件,这些基本都要靠iframe加载你的页面。一加上DENY,客服弹窗直接白屏。这种问题特别隐蔽,前期测试一般发现不了,等业务方反映"页面打不开"你就得排查。所以我的习惯是:业务上线后观察一段时间,确认确实没有iframe嵌套需求,再考虑收紧到DENY。另外说一句,SAMEORIGIN对不同端口是宽容的,只要域名相同,端口不同也可以嵌套。如果你有严格跨域需求,就得配合CSP的frame-ancestors指令来做更细粒度的控制。2.4 Content-Security-Policy(CSP)这个是安全头里的重量级角色。CSP的作用是给浏览器立规矩:你的页面允许从哪里加载脚本、样式、图片、字体、媒体,能不能用eval执行字符串形式的代码,等等。CSP配置的边界很宽,可以用一段字符串就实现复杂的限制策略。先给一个常见的基础配置,适合大多数没有太多第三方资源需求的站点:add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'self'" always;我把每条指令的作用讲透。default-src 'self'是兜底策略,如果后续指令没覆盖到的资源类型,默认只允许同源加载。script-src控制了脚本来源,这个是最容易出问题的。'self'表示同源的脚本可以执行;'unsafe-inline'表示允许内联脚本执行,也就是<script>标签里直接写代码的那种。为什么我不直接禁止unsafe-inline?因为现在很多第三方组件,比如某些验证码、埋点脚本、微信分享SDK都依赖内联脚本。如果你一上来就禁,页面可能白屏或者功能缺一块。稳妥做法是先允许内联,跑通业务再逐步收紧。有个常见的误区得纠正:很多人以为CSP里的script-src能挡XSS的全部可能,但实际上只要你在script-src里写了'unsafe-inline'或'unsafe-eval',XSS防御效果就大打折扣。所以在生产环境,那些能去掉unsafe-inline的场景,尽量去掉。object-src 'none'是禁止<object>标签加载Flash或其他插件资源,这个建议禁掉,没什么正当业务需求会用到。frame-ancestors 'self'其实和X-Frame-Options干的是同一件事,但从策略上更精细。如果你配置了frame-ancestors,它会被现代浏览器优先采纳,X-Frame-Options只是作为旧浏览器兜底。配置CSP最大的风险是误伤业务。这个头一旦配太严,线上会出现各种"神秘问题"——比如某个功能按钮突然无效了,或者图片都裂了,其实都是被CSP拦截了。所以我的建议是:先用Content-Security-Policy-Report-Only模式上线监控一段时间。这个模式下浏览器只上报违规行为、不会真的拦截,你能从report-uri或report-to指令指定的地址收到违规报告,再根据报告逐步收紧策略。2.5 X-XSS-Protection严格来说,X-XSS-Protection在现代浏览器里已经逐步废弃了。因为CSP已经能提供更全面的XSS防御,而且某些浏览器的反射型XSS过滤器反而可能引发问题。但为什么我还是会提到它?因为部分旧版浏览器、部分企业内部环境或依赖旧内核的边缘设备,还认这个头。当作兼容方案可以配置,但别指望它能当主力防护。兼容性配置:add_header X-XSS-Protection "1; mode=block" always;1; mode=block的意思是:启用XSS过滤器,如果检测到反射型XSS攻击,就阻断页面渲染。要注意的是,这个头在某些浏览器版本上会跟CSP冲突,而且如果CSP里允许了unsafe-inline,这个过滤器在某些场景下反而可能把本来安全的页面误判为攻击页面。所以我的处理是:新项目直接不配这个头,靠CSP来做保护;老项目或者对内网环境特别多的情况,才保留这个兼容配置。你如果看到网上有人说一定要配,多半是从2014年左右的文章复制下来的,时代已经变了。2.6 Referrer-Policy这个头控制的是:浏览器在跳转和请求外部资源时,要不要带上来源页面地址——也就是Referer字段。Referer泄露的问题比你想象的大。页面上有一个外部图片,浏览器请求这个图片时自动带上当前页面的完整URL。如果这URL里带了查询参数、用户ID、甚至临时票据,对外就全暴露了。有些行业法规对隐私这方面要求很严,所以Referrer-Policy也算合规层面的一个安全保障。推荐的宽松又有普适性的配置:add_header Referrer-Policy "strict-origin-when-cross-origin" always;这个值是默认的推荐值,含义是:同源请求带上完整URL;跨域请求只带来源域名(不带路径和查询串);从HTTPS跳到HTTP的请求不携带任何Referer。既保证了业务功能正常——比如很多站点的统计系统需要Referer来判断来源——又不会把敏感URL暴露给第三方。如果你做的是一个隐私敏感型产品,比如医疗、支付相关,可以更激进一点,直接用no-referrer,完全不携带来源信息。副作用就是:你可能丢失部分场景下的来源分析数据,某些单点登录流程在旧版本浏览器上也可能受影响。2.7 Permissions-Policy这个头的前身是Feature-Policy,现在规范名已经改成Permissions-Policy。它能控制浏览器的一系列API权限:摄像头、麦克风、地理位置、传感器、全屏、支付等等。现实场景很有用。比如你做的是一个营销页面,完全不需要摄像头和麦克风,那就可以在服务器直接禁掉这些权限,就算页面里的恶意脚本想调摄像头,浏览器直接拒绝,不给任何提示和机会。推荐配置:add_header Permissions-Policy "camera=(), microphone=(), geolocation=(), interest-cohort=(), browsing-topics=()" always;括号里留空表示完全禁止。如果想要只允许当前同源页面使用,而不允许iframe里嵌套的页面滥用,就写成camera=(self),允许同源的iframe使用。我特别解释一下browsing-topics=()和interest-cohort=()。这俩跟广告追踪相关,禁止之后可以在浏览器层面阻断某些隐私追踪机制。如果你做的站点有广告位,可能需要区别于上述配置来调整,否则广告系统可能拿不到投放策略所需的数据。但如果想给用户更好的隐私保护,建议保持禁止状态。配置这个头的时候有个坑:不同浏览器对指令的支持程度不一样。Chrome支持得很好,某些基于Chromium内核的浏览器也还行,但个别浏览器对不认识的指令会直接忽略掉整个响应头。所以你在配置前,最好先在目标浏览器上访问一个/headers类型的测试页面,实际看看各个指令的生效情况再上线。2.8 Cross-Origin-Resource-Policy这个头最容易混淆,但非常管用。它和Access-Control-Allow-Origin最容易搞混。Access-Control-Allow-Origin是配合CORS用的,面向的是跨源资源请求的授权机制;Cross-Origin-Resource-Policy(CORP)则是一个更强的隔离机制,它决定浏览器是否允许其他源加载你的资源。你可以这么理解:CORS是白名单,告诉浏览器"这些来源可以跨域拿我的数据";CORP是黑名单,告诉浏览器"除了我允许的,其他来历不明的请求都不许加载我的资源"。配置推荐:add_header Cross-Origin-Resource-Policy "same-origin" always;same-origin表示只允许同源页面加载资源。如果你的系统还有比较多的跨源图片热链接需求,比如用户上传的图片需要嵌入到小程序或其他业务站点里展示,那就得用cross-origin或者same-site,具体情况具体分析。这个头真正的价值在于防范跨源信息泄露,也就是Spectre这类侧信道攻击时代的遗留防护思路。虽然现在普通项目不一定遇到那么高端的攻击,但加上它成本接近于零,对加固整体安全很有帮助。3. 生产环境配置实操理论说了一大堆,接下来给可以直接用的配置。我把这些安全头整理成一个Nginx片段,分模块讲解,然后教你如何在不影响业务的情况下平滑上线。3.1 完整的安全头配置块这是我在生产环境实际使用过的配置模板,适用于绝大多数Web站点。放在server块里:server { listen 80; listen 443 ssl http2; server_name example.com; # 强制HTTPS访问,HTTP请求直接301跳转 if ($scheme = http) { return 301 https://$host$request_uri; } # 安全响应头集合 add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always; add_header X-Content-Type-Options "nosniff" always; add_header X-Frame-Options "SAMEORIGIN" always; add_header Referrer-Policy "strict-origin-when-cross-origin" always; add_header Permissions-Policy "camera=(), microphone=(), geolocation=(), interest-cohort=(), browsing-topics=()" always; add_header Cross-Origin-Resource-Policy "same-origin" always; add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'self'" always; # 其余业务配置…… }如果你有多个server块,所有需要安全加固的站点都要重复这一段,或者抽取成独立配置文件后用include引入。我的经验是放在conf.d/security-headers.conf文件里,然后各个应用站点的server块里include一份,维护成本会低很多。3.2 不同业务形态的配置差异配置不是一套全打天下,不同类型业务要微调。纯静态站点/内容型网站:资源都来自自己的CDN,没太多第三方脚本,CSP可以做得非常严格,直接去掉unsafe-inline,所有第三方来源都显式白名单化。这类站点配置能收紧到极致,对SEARCH引擎的友好度也不受影响。管理后台类应用:要小心X-Frame-Options和frame-ancestors,如果后台有iframe嵌套编辑器、文件上传预览、富文本组件,要确保CSP的策略允许这些内联资源和iframe来源,否则功能直接崩。我的经验是后台系统的CSP先放宽,稳定运行后再逐步收紧。涉及小程序/公众号开发的服务端:这类场景经常需要被外部平台以iframe方式嵌入,X-Frame-Options不能直接SAMEORIGIN完事,要结合frame-ancestors把对应平台域名放进来。如果只配了SAMEORIGIN,在第三方平台里打开你的落地页就是空白。高流量CDN加速场景:如果你用CDN,安全头通常在源站配一次,然后CDN缓存并下发。但有的CDN配置会主动覆盖源站的响应头。所以上线之后必须用curl -I或者在线Header检测工具确认最终用户实际拿到的是不是你配置的响应头。我之前就吃过这个亏:源站配了HSTS,CDN那边默认给覆盖掉了,导致安全合规检测怎么都不过关。3.3 用curl验证响应头配置完先别急着上线,用curl看一眼响应头对不对:curl -I https://example.com正常的输出会包含这些关键行:HTTP/2 200 strict-transport-security: max-age=31536000; includeSubDomains; preload x-content-type-options: nosniff x-frame-options: SAMEORIGIN referrer-policy: strict-origin-when-cross-origin permissions-policy: camera=(), microphone=(), geolocation=(), interest-cohort=(), browsing-topics=() cross-origin-resource-policy: same-origin content-security-policy: default-src 'self'; script-src 'self' 'unsafe-inline'; ...注意,curl的输出里响应头名的字母大小写不重要,HTTP头本来就是大小写不敏感的。如果你看到某一行缺失,先检查add_header指令是否写错,再看看有没有被其他嵌套的location块覆盖。Nginx的add_header指令在location级别有自己的继承规则,如果你在location里也写了add_header,那么server级别的add_header就全部失效,这一点极其容易被忽视。3.4 分阶段上线策略直接一次性把所有安全头全部上线,风险很大,尤其是CSP和Cross-Origin-Resource-Policy,很容易误伤正在运行的功能。我的做法分三步:第一步,选取一个非核心的环境,比如预发环境,把全部安全头加上,跑一周。期间请业务方尽量完整测试主要功能。第二步,全量上线的时候,先把CSP切到Content-Security-Policy-Report-Only模式,这个模式下浏览器只会往你指定的地址上报违规事件,不会真的拦截任何资源。配置方式是在现有CSP响应头前加Content-Security-Policy-Report-Only,你可以理解成"只记错误日志,不打断业务"。配合report-uri把违规上报收集起来:add_header Content-Security-Policy-Report-Only "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; report-uri /csp-report;" always;其中/csp-report需要你服务端搭建一个简单的接收POST请求的接口,把接收到的数据存到日志或数据库里,我这边是用一个简单的PHP脚本接收并落日志,改造成本很低。第三步,根据上报的违规报告调整策略,确认没有漏网之鱼,再把Report-Only去掉,换成正式生效的Content-Security-Policy。4. 常见问题与排查技巧实录安全头的坑,几乎全集中在"配完之后业务出问题"和"配了但没生效"这两类。下面把我这些年踩过的坑整理成速查表,每一条都对应真实发生过的情况。4.1 配置了安全头但Nginx没生效场景描述:明明在server块里写了add_header,用curl测试却发现对应的响应头没出现。排查思路:先看有没有location块内重复定义add_header。Nginx的变量继承规则是:如果当前location块内声明了任何add_header,那外层的server块add_header就会被全部舍弃。比如:server { add_header X-Frame-Options "SAMEORIGIN" always; location /api/ { add_header X-Request-ID $request_id; proxy_pass http://backend; } }这种情况下,/api/路径的响应里只会出现X-Request-ID,而不会有X-Frame-Options。解决方案有两种:要么把外层的安全头逐个复制到内层location,要么用add_header指令的继承能力提升到http块。我的建议是,把固定的安全头放到http块这个最高层级,因为http块声明的安全头会被所有server和location继承,只有当server或location自己有add_header时才会覆盖。4.2 加了CSP后页面样式全乱场景描述:上线CSP之后,整个页面排版崩掉,但控制台日志里明明显示CSP违规上报。根因分析:style-src没有放行内联样式。很多前端框架、动态主题系统会往<head>里注入<style>标签或直接在元素上写style="..."属性。如果你的style-src默认值过严,这些样式全部被拦下来。我之前用某个后台模板的时候,主题换色功能依赖动态内联样式,配了严格CSP之后换色功能直接失灵,查了半小时日志才反应过来。解决方案:在style-src里加上'unsafe-inline'。如果你实在不想放开内联样式,就得让前端把所有动态样式改成引用外部CSS类,这对很多老项目来说改动太大,得不偿失。4.3 HSTS配置后子域名证书报错场景描述:某天新增了一个子域名,用的证书链没有配完整,在HTTP跳转HTTPS时因为浏览器强制HSTS而直接跳过了HTTP阶段,导致用户看到"连接不是私密连接"的报错页面,而且这个缓存效果还会持续一段时间。排查思路:这就是includeSubDomains的副作用之一。浏览器认了HSTS之后,压根不再发出HTTP请求,你的Nginx跳转逻辑压根走不到,直接死在证书校验环节。所以务必确保所有子域名在启用HSTS之前证书都是有效且完整的。我的习惯:新子域名的证书问题,我都是先在本地hosts里指向预发环境验证,确认证书链没问题之后才切生产DNS。切完之后再通过Chrome的chrome://net-internals/#hsts工具手动查询域名状态,必要时先用delete domain security policies清掉本地缓存再测试。4.4 Permissions-Policy配置导致业务功能异常场景描述:配置之后,某个页面上的"扫一扫"或"定位"功能突然失效。根因分析:之前提到Permissions-Policy会关闭摄像头的camera()权限,但这个配置如果你直接用了默认空白括号,且你的前端页面确实调用了摄像头,那业务就会在毫无提示的情况下失败。因为浏览器不会弹窗询问用户授权,它直接认为权限被策略禁止。解决方案:确认这个页面确实需要摄像头/麦克风权限,把对应的权限指令放宽。比如改成camera=(self),表示只有当前同源页面允许使用摄像头。这样即使用iframe嵌入到其他站点,摄像头也不会被滥用。4.5 Referrer-Policy配置丢失来源数据场景描述:配置了strict-origin-when-cross-origin之后,埋点系统里的来源分析全变成direct或空值。排查思路:这个值相比完全禁止Referer已经温和很多,跨域情况下它至少还会带上域名。所以你丢失的要么是同源页面之间的跳转来源细节,要么是目标站点本身不解析跨域Referer域名。如果业务确实需要收到完整的跳转来源URL,比如分析某个渠道广告的落地页参数,可以考虑用unsafe-url,但强烈不推荐在生产环境这么做,因为URL里很可能带用户标识等参数。4.6 用在线检测工具验证最终链路推荐两个我常用的验证方式:一是浏览器开发者工具的网络面板,拿真实浏览器看响应头;二是用公开的HTTP安全头检测服务,这类服务会模拟主流浏览器发送真实请求,把你响应头里的缺失项和高风险项列出来。值得提醒的是,在线检测服务本身会发起真实请求到你的源站。如果你的源站做了基于UA或IP的拦截,检测结果可能不准。所以最好在预发或测试环境先用curl做一轮基础确认,再上第三方检测工具。4.7 老项目兼容性问题的兜底方案要是你的项目特别老,浏览器环境复杂,没法一步到位直接全部配置,我的兜底方案是先只上三个改动最小的头:X-Content-Type-Options、X-Frame-Options和Referrer-Policy。这三个几乎不会带来什么功能副作用,效果却很明确。等稳定了,再加CSP,最后加HSTS和Permissions-Policy。这个顺序是我在实践中试出来的,能最大程度避免线上事故。个人在实际项目中的体会是:安全头配置的上限不在Nginx,而在浏览器和业务兼容性之间要找平衡。你得先能定位到"哪个资源被哪个头拦了",才能决定是放松策略还是修改业务。如果想降低排查成本,最好在服务端把安全头的版本号打出来,比如在响应头里附带一个自定义字段表明配置版本,这样线上任何一条告警都能快速定位到是不是最近改了安全策略导致的。最后再分享一个小技巧:add_header指令是支持变量的,你可以通过环境变量来区分预发和生产的安全级别。在预发环境暴力配置全禁,在生产环境温和配置,出了事容易对比。上线前用浏览器开发者工具和在线检测扫一遍响应头,再对照业务功能清单过一遍主流程,基本就不会出大问题了。