用 Nginx 配置安全响应头的时候,很多人会遇到 add_header 指令明明写了,浏览器却看不到对应头部的奇怪现象。归根结底,大多数情况下是 always 参数没加、配置位置写错了,或者和代理场景产生了冲突。今天这篇文章把 always 参数不生效的 5 个最常见原因逐一拆解,并给出对应的修复方案。
一、always参数到底是什么
Nginx 的 add_header 指令默认只在请求返回正常状态码(2xx、3xx)时才会把响应头加进去。如果你想让错误页面(4xx、5xx)也带上自定义头部,就必须加上 always 参数。
# 只对 2xx/3xx 生效
add_header X-Custom-Header "value";
# 加了 always,4xx/5xx 也会带上这个头
add_header X-Custom-Header "value" always;
这个区别是理解所有排查问题的前提——先确认你是否真的需要 always,再确认它有没有被正确写进配置里。
二、5个常见不生效原因
1. 根本没写 always,只在错误页面看不到
这是最常见的情况。开发者在 server 块顶部配置了 add_header,但测试 404 页面时发现头部不见了。原因是 server 块顶层的头部只在 2xx/3xx 响应中生效,Nginx 遇到错误会跳转到 error_page 对应的内部 location,而那个 location 没有自己的 add_header,所以头部就丢了。
server {
listen 443 ssl;
server_name example.com;
ssl_certificate /path/to/cert.pem;
ssl_certificate_key /path/to/key.pem;
# 这个头部只在正常响应中生效
add_header Strict-Transport-Security "max-age=31536000" always;
error_page 404 /404.html;
location = /404.html {
# 如果不单独加 add_header,404 响应就不会带上面的 HSTS 头
internal;
}
}
修复方法很简单:要么在 error_page 对应的 location 里也加上 add_header,要么直接用 always 参数一劳永逸。
2. add_header写在HTTP server块而不是HTTPS server块
当你同时监听 HTTP 和 HTTPS 时,很多人习惯在 HTTP server 块里写重定向,同时在 HTTPS server 块里配置 SSL 证书和安全头。如果 add_header 不小心写在了 HTTP server 块里,HTTPS 请求根本不会经过那个配置,自然看不到效果。
# HTTP server —— 只做 301 重定向
server {
listen 80;
server_name example.com;
return 301 https://example.com$request_uri;
}
# HTTPS server —— 安全头要写在这里
server {
listen 443 ssl;
server_name example.com;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header X-Frame-Options "SAMEORIGIN" always;
}
3. 在proxy_pass上游响应中丢失
当你用 proxy_pass 把请求转发给后端时,Nginx 的行为会发生变化。代理场景下 add_header 只在 Nginx 自身产生响应时才会生效——如果你用 error_page 做了内部重定向,然后又 proxy_pass 到上游,add_header 可能就不再生效。
location / {
proxy_pass http://127.0.0.1:8080;
add_header X-Frame-Options "SAMEORIGIN" always;
}
更复杂的场景是 proxy_set_header 和 add_header 同时存在时的继承问题。如果你在父层级有 add_header,但在 location 层级又有 proxy_pass,location 层的 add_header 会覆盖父层级——而不是合并继承。
4. add_header在if条件块中不生效
很多人喜欢在 if 条件里写 add_header,但 Nginx 的 if 指令在某些请求阶段不按预期执行。特别是当你用 if 判断变量然后添加头部时,这个头部可能根本不会被输出。
# 这种写法在某些 Nginx 版本中行为不稳定
location / {
if ($request_uri ~* \.json$) {
add_header Content-Type "application/json" always;
}
}
# 推荐做法:使用 map 指令代替 if
map $request_uri $custom_content_type {
~*\.json$ "application/json";
default "";
}
server {
add_header Content-Type $custom_content_type always;
}
如果必须在 location 里根据条件加头,建议改用 map 指令配合 always 参数的方式,行为更稳定可靠。
5. 父层级的响应头覆盖了子层级的配置
Nginx 的 add_header 继承规则比较特殊:当同一个响应头上出现多个 add_header 配置时,只有最后一个生效。子层级不会追加父层级的头部,而是直接覆盖。
# 父层级
add_header X-Frame-Options "DENY";
location /api {
# 子层级覆盖了父层级的 X-Frame-Options
add_header X-Frame-Options "SAMEORIGIN";
# 父层级的 CSP 头在这里反而丢了,因为子层级没有重复定义
}
location /static {
# 子层级没定义任何 add_header
# 继承了父层级的 X-Frame-Options 和 CSP
}
要解决这个问题,最好的办法是在子层级把所有需要的安全头都显式写出来,或者在需要继承的场景下使用 headers-more 模块来追加而不是覆盖。
三、排查思路总结
遇到 always 参数不生效,按这个顺序检查基本能定位问题:
- 确认是否真的需要 always:测试 200 响应有没有头?没有说明连基本配置都还没生效;有而 404 没有,说明是 always 参数缺失。
- 检查配置写在哪个 server 块:HTTPS 和 HTTP 的 server 块是完全隔离的配置,检查当前请求实际命中的是哪个块。
- 确认是否有 proxy_pass:代理场景下的头部处理规则不同,需要单独在 location 层配置。
- 把 if 改成 map:用 map 指令替代 if 条件来设置变量,行为更稳定。
- 检查头部覆盖问题:父子层级的同名 add_header 不会合并,确认是否被覆盖。
四、推荐的标准配置模板
给你一个可以直接用的 HTTPS server 块模板,包含了最常用的安全响应头:
server {
listen 443 ssl;
server_name example.com;
ssl_certificate /etc/nginx/ssl/cert.pem;
ssl_certificate_key /etc/nginx/ssl/key.pem;
root /var/www/html;
index index.html;
# 安全响应头全部加上 always
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
error_page 404 500 502 503 504 /error.html;
location = /error.html {
internal;
}
location / {
try_files $uri $uri/ =404;
}
}
这样配置的好处是:正常页面和错误页面都会统一带上所有安全头,不需要在每个 location 重复写 add_header。
相关推荐
版权声明
本文仅代表个人观点。
本文系AI辅助作者原创,未经许可,转载请保留原文链接。

发表评论