Linux Nginx 怎么在代理时丢弃不需要的客户端请求头

发布时间:2026/10/4 17:41:51

Linux Nginx 怎么在代理时丢弃不需要的客户端请求头 前言先纠正一个很常见的误解nginx 在反代时默认并不丢弃客户端请求头恰恰相反它会把客户端发来的请求头原样转发给上游。官方文档里proxy_set_header那段只说默认只重定义了 Host 和 Connection 两个字段言下之意是其余字段照原样透传。所以怎么丢弃这件事从来不是默认行为而是需要显式配置的动作。这个默认值带来两个真实后果。第一任何客户端都能自己塞一个X-Forwarded-For: 1.2.3.4或X-Real-IP进来如果上游应用信任这类头做审计、限流或 IP 白名单就形成了伪造漏洞。第二上游会收到一堆它根本不需要的头客户端代理留下的Proxy、扫描器探测用的X-Original-URL、以及和上游协议不一致的Connection轻则日志噪音重则行为异常。本文讲清楚三件事nginx 默认转发了什么、用什么指令把某个头摘掉、以及proxy_set_header那套全有或全无的继承规则是怎么把配置写崩的。示例基于 nginx 1.20RHEL 9 AppStream/ 1.22Debian 12及更新的 1.24、1.26这些指令在以上版本中语义一致。一、请求头与响应头是两条独立的通道新手最容易搞混的一点改请求头和改响应头用的是完全不同的指令方向不能错。指令作用方向典型用途proxy_set_header客户端请求 → 上游改写、追加、丢弃请求头proxy_pass_request_headers客户端请求 → 上游一刀切off时一个客户端头都不转发proxy_hide_header上游响应 → 客户端隐藏上游返回的响应头proxy_pass_header上游响应 → 客户端默认被藏起来的头Date、Server 等放行只记住一句要处理客户端发出去的东西用proxy_set_header。二、把请求头丢掉两个层次的手段2.1 单个字段把值设成空字符串这是最精确的做法也是官方文档明确写的语义——proxy_set_header的值是空字符串时该字段不会发送给被代理的服务器proxy_set_header X-Real-IP ; proxy_set_header X-Forwarded-For ; proxy_set_header Proxy ;注意是空字符串不是不写这一行。不写 不干预 客户端发什么上游收什么写 明确移除。这一字之差就是防伪造能不能生效的分水岭。同理把值重写成 nginx 自己算出来的真实值也能达到丢弃客户端版本的效果而且比直接删掉更有用proxy_set_header X-Forwarded-For $remote_addr; # 整体替换客户端塞的值被覆盖这里必须提醒一个广泛存在的错误写法proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; # 追加不是替换$proxy_add_x_forwarded_for的语义是把客户端 IP 追加到已有的 X-Forwarded-For 后面。如果客户端发来X-Forwarded-For: 9.9.9.9上游收到的是9.9.9.9, 真实IP。真实的那个在最后而绝大多数应用取的是第一个——于是伪造成功。要么改用$remote_addr要么确认上游取的是最后一个值。2.2 全量一个客户端头都不转发如果上游是纯粹的内部服务不需要任何客户端信息可以用总开关location /internal-api/ { proxy_pass http://backend; proxy_pass_request_headers off; # 默认是 on }注意off之后下面这些proxy_set_header依然生效——它们定义的是发给上游的头与客户端是否转发无关location /internal-api/ { proxy_pass http://backend; proxy_pass_request_headers off; proxy_set_header Host $host; # 仍然会发给上游 proxy_set_header X-Real-IP $remote_addr; # 仍然会发给上游 }也就是说proxy_pass_request_headers off加几条proxy_set_header是给上游一个干净、已知的请求头集合的稳定写法。三、继承规则一个 location 打翻整个 server这是本节最值得反复看的一条。proxy_set_header的继承是全有或全无这些指令仅在当前层级没有定义任何 proxy_set_header 时才继承上一层的配置。也就是说你在location里写了哪怕一条proxy_set_headerserver和http层写的所有proxy_set_header就全部失效取而代之的是 nginx 的内建默认值Host $proxy_host、Connection close。一个非常典型的翻车现场server { server_name api.example.com; proxy_set_header Host $host; # 打算让上游知道真实域名 location /v1/ { proxy_pass http://127.0.0.1:8080; proxy_set_header X-Request-Id $request_id; # 只加了一条 # 结果Host 变成 $proxy_host也就是 127.0.0.1:8080 } }上游拿到的 Host 变成了127.0.0.1:8080基于域名做路由或签名的应用立刻报错。修法只有两种要么在这个 location 里把需要的东西全部重写一遍要么把proxy_set_header提到一个所有 location 都能继承的层级上并且保证 location 里一条都不写。出于这个原因实践中推荐把所有proxy_set_header集中定义在一处http或server层location 里只写proxy_pass。需要差异化的地方用include引入一个完整的头集合片段而不是补一条。理解Host三种取值也是必要的变量含义$host请求行或 Host 头里的主机名已转小写、去掉端口取不到时回退到server_name$http_host客户端发来的 Host 头原样带端口$proxy_hostproxy_pass里写的主机名用 upstream 块时是 upstream 的名字实战一份可直接用的反代头配置以下配置以 Debian 12nginx 1.22站点文件在/etc/nginx/sites-enabled/为例RHEL 9 把server块放进/etc/nginx/conf.d/api.conf即可其余一样。# /etc/nginx/conf.d/api.conf upstream api_backend { server 127.0.0.1:8080; keepalive 32; # 配合下面 Connection 复用长连接 } server { listen 80; server_name api.example.com; # 关键所有 proxy_set_header 集中在 server 层 # 保证下面每个 location 都不再单独定义继承才生效 proxy_http_version 1.1; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; # 替换不是追加 proxy_set_header X-Forwarded-For $remote_addr; # 客户端伪造的值被覆盖 proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-Host $host; proxy_set_header X-Request-Id $request_id; # 丢掉客户端可能塞进来的、上游不该信的头 proxy_set_header X-Forwarded-Server ; proxy_set_header X-Original-URL ; proxy_set_header X-Rewrite-URL ; proxy_set_header Proxy ; location /api/ { proxy_pass http://api_backend; # 注意这里一条 proxy_set_header 都不能写 # 写了就会丢掉上面全部继承来的设置 } location /healthz { proxy_pass http://api_backend; } }如果某个路径确实需要长连接到上游keepalive生效的前提Connection头要单独处理location /api/ { proxy_pass http://api_backend; # 这里必须写 proxy_set_header代价是放弃继承所以把需要的都列全 proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $remote_addr; proxy_set_header Connection ; # 空值 删除长连接才能复用 }而 WebSocket 场景需要相反的值用map按客户端的Upgrade头动态决定# 放在 http 块里 map $http_upgrade $connection_upgrade { default upgrade; close; }然后在 location 里写proxy_set_header Connection $connection_upgrade;。注意Connection: upgrade和Connection: 是互斥的两个需求前者要升级协议后者要复用上游长连接同一个 location 里不能既要又要。验证让上游把收到的头打印出来不要靠猜直接看 nginx 究竟发了什么。用一个裸 socket 当上游它会原样打印收到的请求行和请求头# 终端 A监听 8080nc.openbsd 语法部分系统是 nc -l -p 8080 # 以本机 nc 的手册为准 nc -l 8080 # 终端 B发一个带自定义头的请求 curl -s -o /dev/null -H X-Secret: 1 -H X-Forwarded-For: 9.9.9.9 \ -H Host: api.example.com http://127.0.0.1/api/ping终端 A 里会看到上游实际收到的完整请求头。逐条核对X-Forwarded-For是不是只剩你的真实 IP、X-Secret是否还在、Host是不是api.example.com。改完配置后务必先校验再重载sudo nginx -t sudo systemctl reload nginx sudo nginx -T | less # 打印合并后的完整配置确认没有别的文件覆盖了你的设置nginx -T这一步很重要因为conf.d和sites-enabled里其它文件的server块可能匹配同一个server_name把配置抢先接管。常见坑点1. 把X-Forwarded-For用追加变量设置❌proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;——客户端伪造的值留在最前面。 ✅ 用$remote_addr整体替换确实需要保留链路时确认上游取的是最后一个地址。2. 在 location 里只补一条proxy_set_header❌ 在server层定义了Host $host在location里又写了proxy_set_header X-Request-Id $request_id结果 Host 变回$proxy_host。 ✅ 继承是全有或全无。要么该层级把需要的头列全要么在这一层一条都不写。3. 用proxy_set_header Host $http_host却没意识到差异❌$http_host会把客户端发来的端口一起带上客户端发Host: evil.com时上游就收到evil.com。 ✅ 明确要知道真实域名就用$host会回退到server_name需要原样透传时才用$http_host并确保server_name严格匹配。4. 把请求头和响应头的指令用反❌ 想隐藏上游返回的Server头却写了proxy_set_header Server ;客户端依旧看到上游的版本号。 ✅ 响应头要用proxy_hide_header Server;。默认被隐藏的响应头有Date、Server、X-Pad、X-Accel-...需要放行时用proxy_pass_header。5. 带下划线的头被静默丢弃反向的坑❌ 上游需要一个X_Api_Key这样的头nginx 无论怎么配都不转发。 ✅ nginx 默认丢弃请求头里含下划线的字段。真要放行必须显式开underscores_in_headers on;放在http或server层。不过更该做的是把上游的头名改成连字符形式。6.Connection: 和 WebSocket 的upgrade冲突❌ 为了上游 keepalive 写了proxy_set_header Connection ;同一台机器上的 WebSocket 路径握手一直返回 400。 ✅ 用map $http_upgrade $connection_upgrade分流或者给 WebSocket 单开一个 location 并写全所有头。7. 想丢掉某条头却写成了不写❌ 以为不在配置里提这个头它就不会被转发。 ✅ 不写等于透传。要移除必须显式赋空值proxy_set_header Proxy ;。8. 用第三方模块的指令却没装模块❌ 抄来more_clear_input_headers Cookie;直接报unknown directive因为这是headers-more-nginx-module提供的默认编译的 nginx 里没有。 ✅ 只用内置指令清 Cookie 用proxy_set_header Cookie ;。要确认手里的 nginx 支持哪些模块用nginx -V看 configure arguments。总结目标写法移除某个请求头proxy_set_header 头名 ;用真实值覆盖客户端伪造值proxy_set_header X-Forwarded-For $remote_addr;完全不转发客户端头proxy_pass_request_headers off;上游长连接复用proxy_http_version 1.1;proxy_set_header Connection ;WebSocket 升级map $http_upgrade $connection_upgradeConnection $connection_upgrade隐藏上游响应头proxy_hide_header 头名;放行下划线头名underscores_in_headers on;验证实际转发的头裸nc当上游抓原始请求或nginx -T看最终配置核心只有两句话nginx 默认透传客户端请求头要丢就显式赋空值proxy_set_header的继承是全有或全无同一层级要么写全要么一条不写。把这两条记住再配上改完先nginx -t再用nc抓一次真实请求的习惯代理层的头问题基本都能在发布前就拦住。
延伸阅读

更多相关文章

2026/10/4 17:41:51

全志T527 MIPI DSI调试复盘:时钟计算、Deskew校准与FPC排线三大坑

MIPI DSI,调过屏的人都知道,这接口最烦人的地方不是协议难懂,而是链路太长:从framebuffer里的像素到屏幕真正点亮,中间隔着显示引擎、时序控制器、DSI控制器、D-PHY、排线、面板驱动IC,任何一个环节掉链子&…

2026/10/4 17:41:51

从Arduino起步:嵌入式开发第一周的实战踩坑与复盘

1. 为什么我的嵌入式之路从Arduino开始:一个不算"专业"的选择 先说结论:如果你是个刚接触嵌入式的小白,第一周把时间砸在Arduino上,绝对不亏。我身边有不少人一上来就抱着STM32数据手册啃寄存器,或者直接上L…

2026/10/4 17:41:51

鸿蒙NEXT接入开源大模型的五大工程决策与实践

最近不少群友在问“鸿蒙NEXT能不能跑大模型”这件事。问多了你会发现,大家真正卡住的不是某个API怎么调用,而是几个更靠前的工程决策:模型服务放在哪、端上通信怎么选、上下文怎么管、并发怎么调度、密钥怎么藏。这些决策在跑通Demo时根本看不…

2026/10/4 18:56:54

从零开始搭建AI工程能力:算法到生产的完整闭环

做AI工程这行久了,经常有朋友问我“怎么从零开始入门”。我一开始没太当回事,觉得照着教程跑通几个模型就算入门了。直到我亲眼看到不少科班出身、算法调参很熟练的同学,在把一个模型真正变成线上服务时被各种工程问题捶得没脾气,…

2026/10/4 18:56:54

LangGraph 从入门到实战(07):多智能体——让一个「主管」把任务派给几个「工人」

LangGraph 从入门到实战(07):多智能体——让一个「主管」把任务派给专业工人 前几篇都是「一个 Agent」。真实业务往往要分工:财务、技术、客服各司其职,由谁接活、派给谁,需要一个「主管」来协调。LangGraph 的多智能体本质就是一张图里放多个节点(每个节点就是一个「…

2026/10/4 18:56:54

AI Agent 缓存实战:Redis 数据结构选型与失效策略

Redis 在 AI Agent 里到底扮演什么角色,这个问题我在过去大半年里被问过不下二十次。每次有人听说我在 Agent 项目里用 Redis 做缓存,第一反应往往是"不就是个 key-value 存储吗,能有多复杂"。但真正把 Agent 跑在生产环境里、面对…

2026/10/4 18:56:54

插件报错排查指南:从加载到激活,一步步定位问题根源

plugins这个词,单独扔进搜索引擎的时候,往往不是出于好奇,而是带着一屏幕的报错来的。热搜里那几条很有意思——“iar plugins 是干什么的”“failed to load plugins web boot: 2 entries did not activate”“harness failed to load plugi…

2026/10/4 0:01:02

Jev+Agent接管浏览器:browser-use实战与jev-ultrafast性能优化

1. 从“Jev”说起:为什么我要把Agent接进浏览器“Jev”这个词最近在圈子里出现的频率越来越高,很多人第一次听到会以为是某个新模型的名字,其实它更像是一种思路——把Jev模型的能力当作底座,通过Agent的方式去接管浏览器&#xf…

2026/10/4 0:01:02

多智能体集群实战:DeepAgents编排、MCP与A2A协议及Skills体系

1. 从"单兵作战"到"集群协同":多智能体编排到底在解决什么问题如果你最近在折腾 Agent 相关的东西,大概率会有一种感觉:单个 Agent 能做的事情,其实很快就摸到天花板了。你给它一个提示词,挂几个工…

2026/10/4 1:01:05

无源低通滤波器设计实战:从RC到LC,手把手教你避开那些坑

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/4 0:01:02

Jev+Agent接管浏览器:browser-use实战与jev-ultrafast性能优化

1. 从“Jev”说起:为什么我要把Agent接进浏览器“Jev”这个词最近在圈子里出现的频率越来越高,很多人第一次听到会以为是某个新模型的名字,其实它更像是一种思路——把Jev模型的能力当作底座,通过Agent的方式去接管浏览器&#xf…

2026/10/4 0:01:02

多智能体集群实战:DeepAgents编排、MCP与A2A协议及Skills体系

1. 从"单兵作战"到"集群协同":多智能体编排到底在解决什么问题如果你最近在折腾 Agent 相关的东西,大概率会有一种感觉:单个 Agent 能做的事情,其实很快就摸到天花板了。你给它一个提示词,挂几个工…

2026/10/4 1:01:05

无源低通滤波器设计实战:从RC到LC,手把手教你避开那些坑

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
☎咨询二维码 ☎ ↑