news 2026/10/1 3:32:00

Nginx反向代理必知:$host、$http_host、$proxy_host三变量深度解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Nginx反向代理必知:$host、$http_host、$proxy_host三变量深度解析

上次帮同事排一个反代问题,前端所有登录跳转都指向内网IP,用户一登录就被踢出去。抓包看后端请求,发现后端收到的Host根本不是用户访问的域名,而是proxy_pass里写的那个内网地址。问题就出在proxy_set_header Host到底该写$http_host还是$host还是$proxy_host上——这三个变量名字像三胞胎,实际在Nginx里各管一摊,写错一个,请求头就变了味。

这篇文章就把这三个变量掰开揉碎讲清楚:它们分别从哪来、取值规则是什么、在反向代理场景下默认会把哪个Host传给上游,以及遇到SSL证书不生效、上游报host not defined这类问题时,该怎么顺着Host头一路排查。适合刚接触Nginx反代的读者,也适合配置过不少反代但从没深究过Host头的同学。看完你不仅知道该写哪个,还能明白为什么这么写。

1. 三个变量看着像,实则在Nginx里各管一摊

1.1 一个真实场景开场:反代后应用拿到的域名不对劲

先说那个让我印象深刻的排错过程。系统架构很简单:用户访问example.com,Nginx做反向代理,把请求转发到内网的一台应用服务器192.168.1.10:8080。问题是用户一登录,页面跳转的地址就变成http://192.168.1.10:8080/login,浏览器直接访问内网地址,当然失败。

最开始怀疑后端应用的域名配置写死了,翻了一圈代码没发现问题。后来在Nginx上抓请求,发现后端收到的HTTP请求头里,Host字段的值是192.168.1.10:8080,压根不是example.com。后端应用拿到这个Host去生成跳转链接,自然就拼出了内网地址。

这就是典型的Host头被改写导致的“反代跳转错乱”。解决方式一句话:在location里加上proxy_set_header Host $host;,让后端知道用户实际访问的是哪个域名。但为什么有人写$host,有人写$http_host,还有人写$proxy_host?这三者在很多场景下输出还是一样的,这就让问题更乱了。

1.2 三个变量各自管什么:先看官方定义

先把官方的定义摆出来,后面再逐个拆解:

  • $http_host:等于客户端请求头中Host字段的原始值,原样保留大小写和端口。
  • $host:按优先级返回“请求行中的主机名、请求头Host字段、匹配到的server_name”中的值,并统一转小写、去掉端口。
  • $proxy_host:来自proxy_pass指令中指定的目标主机名和端口。

只看定义就能发现,前两个变量描述的是“客户端要访问谁”,第三个变量描述的是“Nginx要把请求转给谁”。一个是“来路”,一个是“去处”,混在一起用自然要出问题。

1.3 为什么这三个变量容易被混用

我观察下来,混用的原因主要有三个:

第一,名字太像。$http_host和$host就差一个前缀,写起来几乎没区别。第二,很多简单场景下它们的值恰好相同。比如客户端直接访问example.com,不带端口,Nginx的server_name也是example.com,那么$http_host和$host都返回example.com,看不出差异,一换复杂场景就露馅。第三,不少教程只告诉你怎么写,不解释为什么,照抄的时候变量就抄乱了。

理解这三个变量,关键在于弄清楚它们的取值来源和计算规则。下面逐个过。

2. $http_host:请求头里的原始Host,原样保留不带修饰

2.1 $http_host到底从哪来:HTTP头到变量的映射规则

Nginx有一类内建变量是用$http_前缀加上请求头字段名组成的。比如Host请求头对应$http_host,User-Agent请求头对应$http_user_agent,Content-Type请求头对应$http_content_type。规则就是把请求头字段名转成小写、把中间的减号换成下划线,然后加上$http_前缀。

所以$http_host的值完全取决于客户端发过来的Host请求头长什么样。客户端发Host: Example.COM:8080,Nginx的$http_host就是Example.COM:8080,一点不改。

这个变量和Nginx自己的配置没有任何关系。你设置了什么server_name、监听哪个端口,都不影响它的取值,它就是对客户端请求头的一次“照抄”。

2.2 $http_host的三个特性和两个坑

三个特性分别是:保留原始大小写、保留端口、可能为空。

保留大小写这个特性,在大多数场景下没啥用,因为HTTP/1.1规范要求Host字段大小写不敏感。但如果你要在Nginx里做精确的字符串比对,比如if ($http_host = "Example.COM"),就会踩坑——客户端大小写一变,匹配就失败了。

保留端口这个特性很实用。用户访问http://example.com:8080,Nginx监听8080端口做反代,$http_host的值是example.com:8080。后端应用拿到这个值,就知道用户是从8080端口进来的,生成跳转链接时不会丢端口。

可能为空的坑,比前两个都隐蔽。HTTP/1.0协议不强制要求客户端发送Host头,一些老客户端、监控探活脚本、自己写的HTTP客户端都可能不带Host头。这时候$http_host就是空字符串。你需要排查“反代后端收到空Host”的问题时,十有八九是这里出了问题。

2.3 什么场景必须用$http_host

最典型的就是涉及非默认端口的反代场景。Nginx监听8080端口,对外域名是example.com,用户实际访问的是http://example.com:8080。如果proxy_set_header Host $host;,后端收到的Host是example.com,端口信息丢了。后端应用生成重定向时默认拼80端口,用户点过去直接404。

这时候用$http_host就对了,端口原封不动传给后端。类似的场景还有:需要在Nginx日志里精确还原用户原始请求域名和端口时,用$http_host记录是最准确的。

3. $host:被Nginx“清洗”过的主机名,取值优先级有讲究

3.1 $host的取值优先级:请求行、请求头、server_name

$host的取值不是简单地从请求头里读,它有明确的三级优先级:

  1. 请求行中的主机名。也就是HTTP请求行里直接带的absolute-form URI,比如GET http://example.com/path HTTP/1.1这种形式。实际业务里很少见,但规则确实存在。
  2. 请求头Host字段的原始值,也就是$http_host的值。
  3. 匹配到当前请求的server_name。

大多数情况下,我们遇到的都是第二种来源:请求头里带了Host,$host就跟着$http_host走。但注意,它跟$http_host的输出不一样,见下一节。

3.2 端口与大小写:自动清洗的底层逻辑

$host和$http_host最大的区别,是$host会做两件事:统一转小写、去掉端口号。

客户端发Host: Example.COM:8080,$http_host返回Example.COM:8080,$host返回example.com。这个设计是有原因的:HTTP/1.1规范定义Host字段不区分大小写,Nginx干脆统一成小写,方便你做路由判断和日志统计。端口单独拆出来是因为$host的定位是“主机名”,不是“主机名加端口”。

举个例子,你做了这样一个路由判断:

if ($host = "Example.com") { return 301 http://www.example.com$request_uri; }

如果直接用$http_host,客户端一旦用Example.COM访问就匹配不上。用$host就稳了,大小写已经被Nginx归一化。

端口被去掉这个特性,在上一节已经提过会导致跳转丢端口。这里再补充一个场景:Nginx前面还有一层负载均衡器,负载均衡器转发请求时把端口信息弄丢了,这时候后端看到的是$host没有端口,但$http_host可能带着端口——前提是负载均衡器在转发时保留了原始Host头。这也是排查问题时需要分辨的。

3.3 通配符、正则server_name下的$host表现

很多初学者以为$host就是server_name的值,这是错的。$host优先取请求头Host,只有当请求头里没有Host时,才退回server_name。

更微妙的是,即使退回server_name,如果你配置的是通配符或正则形式,$host也不是返回配置里的那个通配符字符串,而是返回实际匹配上的server_name。比如这样的配置:

server { listen 80; server_name *.example.com; # ... }

客户端请求Host: foo.example.com,$host的值是foo.example.com,而不是*.example.com。

而$server_name这个变量,返回的恰好是配置里写的*.example.com。所以如果你在日志里看到$host和$server_name不一样,别奇怪,它们的定义就决定了结果不同。

3.4 $host和$server_name不是一回事

把这两个变量放在一起对比,能帮助理解:

  • $host:用户实际请求的主机名(清洗过),跟用户走。
  • $server_name:Nginx配置里匹配到的server块的名字,跟配置走。

当请求头Host为foo.example.com、server块配置为*.example.com时,$host是foo.example.com,$server_name是*.example.com。反过来,当客户端请求没带Host头时,$host会回退成$server_name的值。所以$host在绝大多数情况下不会为空,这是它做反代Host头时比$http_host更稳的原因。

4. $proxy_host:只认proxy_pass的目标主机,和客户端请求无关

4.1 $proxy_host的定义:来自proxy_pass的目标主机

$proxy_host和前两个变量完全不是一个体系。它不关心客户端请求了什么域名,只认proxy_pass指令里写的那台目标主机。

比如配置是这样:

location /api/ { proxy_pass http://10.0.0.5:8080; }

那么$proxy_host就是10.0.0.5:8080。如果你写的是:

location /api/ { proxy_pass http://backend_upstream; }

backend_upstream是一个upstream组的名字,那$proxy_host的值是backend_upstream,而不是组里某个真实后端的IP。

这个细节很多人不知道。默认情况下,如果你不写proxy_set_header Host,上游服务器收到的Host头恰恰就是$proxy_host的值。换句话说,客户端明明访问的是example.com,上游看到的却是10.0.0.5:8080或者backend_upstream这种自己人才能看懂的名字。

4.2 默认行为:Nginx到底把哪个Host传给上游

这是全文最核心的知识点之一。Nginx官方文档写得很清楚:默认情况下,proxy模块会重新定义Host请求头,默认值就是$proxy_host。

也就是说,不做任何显式配置时,上游收到的Host不是你网站的域名,而是proxy_pass里写的那个目标主机。开篇那个排错案例,根因就在这里。很多反代导致后端应用跳转错乱、域名校验失败、链接生成错误的问题,都是因为这个默认行为。

理解了这一点,再看网上各种教程里写的proxy_set_header Host $host;,本质就是在覆盖这个默认行为,让上游知道用户真正访问的域名。同理,proxy_set_header Host $http_host;则是连端口、大小写一起原样透传。

4.3 proxy_pass带变量时,$proxy_host会发生什么

还有一个进阶场景。当proxy_pass里的目标不是写死的,而是用变量拼出来的,比如:

location / { proxy_pass http://$backend_addr; }

这时候$proxy_host的值不再是配置加载时就确定的静态主机名,而是请求处理时$backend_addr这个变量解析出来的值。它可能是10.0.0.5:8080,也可能是10.0.0.6:8080,取决于你的map或if逻辑。

这种动态反代写法下,默认的Host头也会跟着$proxy_host变。如果你想让上游始终收到固定的Host,就必须显式写proxy_set_header Host。另外注意,proxy_pass里使用变量时,Nginx有可能要求你在http块里配置resolver来解析域名,因为配置加载阶段无法确定目标主机。

5. 实际配置时到底用哪个:反代、路由分发、日志三类场景全拆解

5.1 场景一:普通反代透传原始域名

最常见的场景:Nginx对外提供服务,反代到后端应用,希望后端看到的是用户访问的真实域名。

server { listen 80; server_name example.com www.example.com; location / { proxy_pass http://backend_app; proxy_set_header Host $host; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-Host $host; } }

这里用$host而不是$http_host,主要考虑两点:一是统一小写,后端做域名匹配时更省心;二是当客户端没带Host头时,$host会回退到server_name,保证上游至少能收到一个合法的域名。

如果后端应用对端口敏感,比如Nginx监听的是8080端口,优先考虑$http_host。

5.2 场景二:固定上游Host,让后端认为请求来自目标域名

有些后端应用会校验Host是否属于自己,不接受任意域名。如果反代的目的是“借用Nginx的域名来访问一个只认固定域名的后端”,需要把Host固定成上游认可的域名:

location / { proxy_pass http://upstream_internal; proxy_set_header Host internal.service.local; }

这种写法下,不论用户访问的是a.example.com还是b.example.com,上游收到的Host都是internal.service.local。$proxy_host在这里反而不合适,因为如果upstream组名是upstream_internal,默认Host就变成了组名,后端还是不认。所以手动写死Host最直接。

5.3 场景三:按Host做流量路由

多域名共用一个Nginx时,经常需要根据用户访问的域名分发给不同后端。这种需求用map加$http_host或$host都能实现,关键在于你想要“带端口判断”还是“纯域名判断”。

map $host $backend_addr { app1.example.com 10.0.0.11:8080; app2.example.com 10.0.0.12:8080; default 10.0.0.10:8080; } server { listen 80; server_name _; location / { proxy_pass http://$backend_addr; proxy_set_header Host $host; } }

这里用$host做map的key,好处是大小写已被归一化,用户用APP1.EXAMPLE.COM访问也能正确路由。如果用$http_host,还得自己在map里兼容大小写变体。但要注意,$host不带端口,如果多个域名在不同端口上提供服务,就要改回$http_host。

proxy_pass里用变量的写法,会让$proxy_host变成动态值,此时建议显式设置Host,避免上游拿到不确定的域名。

5.4 场景四:日志和分析中该记哪个

Nginx的access_log可以通过log_format自定义字段。我建议按用途选择:

  • 域名维度的流量统计、QPS分析,用$host。因为值稳定,不会因为端口、大小写产生多余的维度,方便聚合。
  • 排查具体用户的跳转、鉴权问题,用$http_host。它能精确还原用户请求的原始Host,包含端口。
  • 排查反代链路,把$host和$proxy_host同时记上,一眼能看出“用户想访问谁”和“Nginx实际转发给谁”。

实际配置示例:

log_format trace '$remote_addr - $host | upstream_host: $proxy_host | req_host: $http_host'; access_log /var/log/nginx/access.log trace;

这样一条日志同时记录三个变量,问题定位效率会高很多。

6. 结合现实报错:SSL证书不生效、证书未发送、内网跳转的排查记录

6.1 案例:“替换SSL证书不生效”的排查链路

有一个热搜词是“nginx替换ssl证书不生效”,我遇到过好几次。先说结论,这类问题大多和三个变量没关系,但排查思路和Host头有千丝万缕的联系,因为TLS握手时客户端发送的SNI本质上就是域名。

现象:改了ssl_certificate指向新证书,nginx -t也通过了,reload之后浏览器看到的还是旧证书。

完整排查链路:

  1. 先用nginx -T | grep ssl_certificate确认当前生效的配置到底是哪个证书文件。有时候你改的是A server块的证书,但用户访问的域名实际命中B server块。
  2. 用openssl命令直接探测:
openssl s_client -connect example.com:443 -servername example.com 2>/dev/null | openssl x509 -noout -subject -dates

-servername参数就是手动指定TLS握手时的SNI。如果你不指定,抓到的可能是不带SNI的default_server证书,自然不是你新替换的那个。

  1. 确认命中正确的server块后,检查证书文件本身有没有更新成功。有时候是软链接指向旧文件,或者证书链文件里只追加了新证书、没有替换干净。
  2. 最后检查浏览器缓存和系统证书缓存。Chrome对HTTP公钥钉扎和证书缓存比较顽固,开无痕窗口测最干净。

这个排查过程里,$host、SNI、server块匹配三者是联动的:Nginx就是靠SNI来选server块、选证书的。如果你在配置层面把多个域名的证书搞混了,或者default_server块在“抢”流量,就会看到“证书不生效”的假象。

6.2 案例:“no required ssl certificate was sent”

这个报错常见于双向TLS场景。Nginx配置了ssl_verify_client on;,要求客户端必须出示证书,但客户端没有发送,握手就会失败并产生这个错误。

排查思路:

  1. 看Nginx错误日志,路径一般在/var/log/nginx/error.log,里面会有完整的TLS握手失败原因。
  2. 确认本端是否配置了ssl_client_certificate,指向CA证书文件。
  3. 确认客户端是否安装了正确的客户端证书,并且证书链完整。
  4. 检查Nginx是否把客户端证书校验要求透传给了上游。如果Nginx做SSL终止,再代理给上游,而上游也开启了客户端证书校验,Nginx本身又没有配置ssl_client_certificate和proxy_ssl_certificate,上游就会给Nginx报这个错。

这里顺带说一句,遇到SSL类报错,先确认你连的是不是正确的域名和端口,再往下查证书。用curl -vI https://域名看一眼握手过程,能少走很多弯路。

6.3 案例:反代后应用跳转地址错乱

回到开篇的问题。后端应用根据Host头生成跳转链接,Host不对就全乱套。根因是默认情况下Nginx把Host设置成了$proxy_host。

解决方式分两种:

  • 后端希望看到原始用户域名:proxy_set_header Host $host;(不要端口)或proxy_set_header Host $http_host;(保留端口)。
  • 后端配置了域名白名单:把Host固定成后端认可的域名。

排查这类问题时,我建议在后端服务器上抓一次HTTP头,看Host字段到底是什么。抓包工具可以用tcpdump,更轻量的做法是在后端临时加个接口把请求头打出来,或者看后端access_log里的请求域名。基本上一眼就能判断是Host设置的问题。

6.4 排查时的通用思路:先分清“来路”和“去处”

综合这几个案例,我总结了一个排查Host相关问题的通用顺序:

  1. 先确定用户实际请求的域名和端口,也就是$http_host应该是什么。
  2. 再确定Nginx配置里匹配到的server_name,也就是$host会回退成什么。
  3. 然后看proxy_pass目标,确认$proxy_host是什么。
  4. 最后看location里有没有写proxy_set_header Host,写了哪个变量。

大多数Host相关的诡异问题,走完这四步都能定位。如果后端报host not defined之类的错误,别急着查应用代码,先看Nginx传给后端的Host是不是空字符串——$http_host在客户端没带Host头时就是空的。

7. 一张对照表加三步自测,下次配置不再犹豫

7.1 一张表记住全部差异

变量取值来源大小写端口可能为空典型用途
$http_host客户端请求头Host原样保留保留可能为空精确还原用户原始请求,排查跳转问题
$host请求行 > 请求头Host > server_name统一小写去掉基本不为空域名路由、日志统计、稳定透传
$proxy_hostproxy_pass目标主机按配置按配置可能为空默认上游Host,用于定位反代转发目标

这张表是我自己整理时最常看的版本。核心记忆点就三条:$http_host是“原样照抄”,$host是“清洗归一”,$proxy_host是“转发目标”。

7.2 三步自测法:用log_format实测

如果你还是记不牢,建议花五分钟实测一把。配置一个临时server,把所有变量打进日志,然后用curl模拟不同Host头,观察输出。

log_format host_test '$http_host | $host | $proxy_host'; server { listen 8080; server_name example.com; location / { proxy_pass http://10.0.0.5:8080; proxy_set_header Host $http_host; access_log /var/log/nginx/host_test.log host_test; } }

然后逐个测试:

curl -H 'Host: Example.COM:8080' http://127.0.0.1:8080/ curl --http1.0 -H 'Host:' http://127.0.0.1:8080/

第一条命令,日志里$http_host是Example.COM:8080,$host是example.com,$proxy_host是10.0.0.5:8080。第二条命令,$http_host是空的,$host回退成example.com。

再把proxy_set_header Host分别改成$host和$proxy_host,去后端看实际收到的Host头。亲眼看到差异,比死记硬背强得多。

7.3 我的几条实操建议

最后分享几条踩过坑之后的经验。

第一,写反代配置时,不要依赖默认行为。默认的上游Host是$proxy_host,绝大多数情况下不是你想要的结果,显式写proxy_set_header Host是必须的。

第二,对外提供服务的反代,优先用$host做Host头;如果服务端口非80、443,改用$http_host,否则端口会丢。

第三,做域名判断、路由分发、流量统计,优先用$host,它已经帮你处理了大小写和端口这两个干扰项。

第四,排查问题不要只盯Nginx配置。客户端可能压根没发Host头,上游可能在做二次跳转,CDN可能改写了Host头。用日志和抓包把整条链路打通,再回来看是哪个环节动了Host。这几个变量的区别说到底就一句话:明白用户想访问谁,清楚Nginx要转给谁,再决定让上游看见谁。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/1 3:31:50

CSV与Pandas高效数据处理:从读写到清洗合并的实践指南

1. 为什么这组搭配绕不开:CSV 和 Pandas 的分工1.1 先看懂 CSV 的真面目CSV(Comma-Separated Values)本质上就是一个纯文本文件,每一行是一条记录,每个字段用逗号分隔。它的历史能追溯到早期表格软件时代,几…

作者头像 李华
网站建设 2026/10/1 3:31:46

难题分级:从项目级到世界级,如何炼成真正的专家?

先说结论:真正的专家是定义和解决难题的过程中锻炼出来的。这些年我接触过不少创业者、技术负责人、行业前辈,也复盘过自己走过的弯路,最大的感触就是——人和人的差距,往往不是学历、背景、资源堆出来的,而是看他主动…

作者头像 李华
网站建设 2026/10/1 3:30:53

基于麒麟操作系统的图书管理系统开题答辩全攻略

如果这学期即将开题的你在深夜点开这篇内容,我猜你多半正经历那种“题目还没想好,后天就要交开题报告”的焦虑。我当初也一样,但走完一遍回头看,发现开题答辩并没有想象中那么可怕——关键在于把这件只有十几分钟的事,…

作者头像 李华
网站建设 2026/10/1 3:30:52

Vivado中ILA位置约束报错:Place 30-638的成因与解决

凌晨两点,Vivado的implement进度条卡在Place Design阶段快十分钟,我点开log,最后一行是ERROR: [Place 30-638] This port location for the ILA core at location 0 is not valid.那一刻我确实有点懵。做FPGA调试时最怕的不是时序收敛不了&am…

作者头像 李华
网站建设 2026/10/1 3:29:27

C++链表核心操作与算法实战:从建节点到反转合并的完全指南

链表这东西,我在之前的练习记里提过一嘴,今天专门拎出来写一篇。原因很简单:链表在C算法题里的出场率实在太高了,而且它和数组、vector那种“一段连续内存”的直觉完全不同,很多新手写起来特别容易栽跟头。我也是从一个…

作者头像 李华
网站建设 2026/10/1 3:29:06

C/C++重复符号错误排查指南:从duplicate symbol到extern声明修复

如果你在编译 OpenClaw(或者任何一个 C/C 项目)的时候,链接阶段蹦出这么一行:ld: duplicate symbol _claw_global_configin claw_config.o and main.o那恭喜你,你踩中了一个经典的全局变量重复定义问题。这类报错在 C …

作者头像 李华