加一行安全响应头,差点把站点搞挂
起因:几个安全响应头,想一次管全站
我在公司做办公室维护,平时管打印机、修电脑,公司内部那几个系统的日常维护也归我。自己的这个小站是下班以后折腾出来的,配置改得越多,胆子反而越小——踩过的坑基本都是一个套路:改的时候觉得没问题,出问题的时候又找不到原因。
前段时间我给站点补了几个安全响应头。想法很直白:页面不希望被别人的 iframe 嵌进去,浏览器别自己去猜文件类型,Referer 不要把完整地址到处带给外站,再加上一点基础的传输安全要求。这些东西不用改页面代码,写在网站服务器配置的最外层就行,一次生效全站。
写完我特地抓了一次响应头,首页有、文章页也有,挺满意,觉得自己这回总算干了件正经事。确认的办法也不复杂:打开浏览器的开发者工具,在网络面板里点开首页那次请求,看响应头那一段,三行都在。我当时还把截图存了下来,想着以后万一有人问起来,好歹能翻出来给人看。
标题里说"差点把站点搞挂",其实是句气话。站点一天都没挂,访问一直好好的。真正让我不踏实的是:我以为设置生效了,它其实早就没了。
后来加了两行缓存,功能上挑不出毛病
过了几天,我发现静态资源在浏览器里老是重新下载,就想着给它们设个缓存时间。做法很常规:在针对某个目录的配置块里加了两行,让图片这类不常变的东西在浏览器里多留几天。改完刷新页面,资源正常加载,缓存时间也对得上,一切看起来都很顺。
顺手说一句,我加的是那种不常改的东西所在的目录,图片、字体这一类。这些文件内容基本不动,让浏览器多留几天,回访的时候就不用重新下一遍。
我当时还在心里夸了自己一句,说这配置挺干净。
它不是叠加,是替换
问题是在我给站点写了个自动检查脚本之后浮出来的。脚本会把几个典型地址都请求一遍,把响应头打出来对照。跑完我发现,首页的安全头都在,可那几个我单独设过缓存的目录,安全头一个不剩。
我一开始以为是脚本写错了,后来手工又抓了一次,确认不是我眼花。
这类服务器软件的规则是:只要在更具体的配置块里出现了一次"添加响应头"这个指令,这个块就不再继承外层的任何响应头。它不是叠加,是替换。
也就是说,我为了设缓存顺手加的那一行,等于替那几个目录宣布"外层的头跟我没关系了"。缓存设对了,安全头全丢了,配置语法完全合法,服务也不会报任何错。
为什么这事特别难发现
页面打开一切正常,功能没有任何异常,没有白屏,也没有 500 报错。安全头缺失不会弹提示,控制台也不会说话。对访客来说,网站跟之前一模一样。
只有真的去抓一次响应头,才看得到那几行不见了。而且抓的位置还得对——你要访问的是那几个目录下的地址,光看首页看不出来。所以我后来把检查脚本改了,每一类路径都单独抓一次,不再只抓首页就收工。
那个脚本本身写得很土:拿几个地址挨个发请求,把状态码和响应头按行打出来,再跟我手写的一份清单对一遍。它不懂什么该有、什么不该有,全凭我列的那几行。也正因为清单里每一类路径都占了一行,这几条头不见了才被我撞上——要是清单里只有首页,这事估计还得再躺很久。
- 报错是好事,至少它告诉你哪里不对;
- 静默失效最麻烦,它什么都不说,你还以为一切照旧;
- 越是"加一行就行"的配置,越值得回头确认一次结果。
三种改法,我挑了最省事的
搞清楚原因之后,路子其实很清楚了:
- 需要单独设缓存的块,只写缓存时间这类指令,不碰响应头;
- 如果确实要在某个块里加响应头,就把那个块需要的头重新写全,别指望外层还罩着你;
- 最省事的办法:安全头统一放在最外层,各目录块一个响应头指令都不写。
我最后选了第三种。站点不大,安全头本来就该全站统一,放外层最省心;目录块只管缓存、跳转这些自家的事,谁也不越界。下面两段是"看着没问题"和"实际更稳"的区别,域名和路径都换成了通用写法。
第二种改法我也试过一小会儿,很快就放弃了。一旦决定"局部把头像重新写全",就等于把同一份清单在两三个地方各抄一遍;哪天要调整其中一条,得记得每一处都改。抄漏一处不会报错,只会继续静默失效——这正是我一开始掉进去的那个坑。
容易出问题的写法:外层有安全头,目录块里又冒出一个添加头的指令。
server {
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;
location /static/ {
# 就是这一行,让上面三行在这条路径下全部失效
add_header Cache-Control "max-age=604800";
}
}
改完之后的写法:目录块只管缓存,不出现任何响应头指令。
server {
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;
location /static/ {
expires 7d;
root /var/www/example/;
}
}
改完再抓一次,几个路径下的头就都回来了。就这么两行的事,前后折腾了我一个晚上。
顺手记下的一条经验
配置的"继承"和"覆盖"规则,每个软件都不一样,而且往往是静默生效的——不报错,不警告,只是悄悄地变了。它不会在你敲回车的时候拦住你问一句"你确定吗",它只会照你说的做。
所以我现在遇到"某个设置有的页面有效、有的页面没有",第一反应就是:是不是被更局部的配置顶掉了。从配置的层级往上找,多半能对上号。
这种"有的有、有的没有"其实还有别的样子:某个设置只在某一个子目录里不起作用,或者同样的配置在新机器上生效、旧机器上没动静。看着像玄学,往上翻两三层配置,通常就能看到某处藏着一次局部覆盖。
这类问题的通用解法,说到底就一句话:加完之后,去实际抓一次结果。别盯着配置文件想"应该没问题"——配置文件是我写的,里面装的是我以为的规则;抓回来的响应头,才是服务器实际干的事。
本文写于 2026-09-13,记录的是当时的情况与做法,未必适用于所有环境,仅供参考。