news 2026/10/1 15:16:56

Nginx反向代理中$host、$http_host、$proxy_host的区别与正确选择

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Nginx反向代理中$host、$http_host、$proxy_host的区别与正确选择

刚入行那阵子,我在线上排查过一个很诡异的故障:后端服务明明一切正常,却一直报“找不到虚拟主机”,日志里连过来的 Host 全是内网 IP 加端口,我当时盯着proxy_set_header Host $proxy_host;这行配置愣了半天。后来才明白,Nginx 里这些“长得很像”的变量——$http_host、$host、$proxy_host——实际上各自代表完全不同的含义,用错一个,行为就差出十万八千里。

这篇文章不绕弯子,直接把这几个变量掰开揉碎讲清楚:它们分别从哪来、在什么场景下用、实战里选错会有什么后果,以及我这些年踩过的一些坑。无论你是刚学 Nginx 的小白,还是已经写了几年配置的老手,看完之后应该都能对这三个变量有个彻底的把握。

1. 三个变量各自扮演什么角色

先说结论:$http_host和$host都来自客户端请求,只不过一个“原封不动”,一个“经过 Nginx 处理和规范化”;而$proxy_host跟客户端请求一点关系都没有,它来自 Nginx 反向代理配置里的上游服务器定义。这三个变量出现在不同阶段,存储的也不是同一个东西。

1.1 $http_host——原样照收的“原始请求头”

$http_host是 Nginx 内置的$http_xxx系列变量之一,这一系列变量代表的都是“客户端 HTTP 请求头中对应字段的内容”。比如$http_user_agent对应 User-Agent,$http_referer对应 Referer,那$http_host自然就对应请求头里的 Host 字段。

这里最关键的一点是:它不会做任何处理,客户端发过来是什么样,它就是什么样。如果客户端发的是Host: example.com:8080,那$http_host的值就是example.com:8080,冒号端口原样保留;如果客户端发的是Host: example.com,那$http_host的值就是example.com,不带端口;如果客户端压根没发 Host 头(比如 HTTP/1.0 的老客户端),$http_host就是空值。

很多人会忽略“原始”这两个字的分量。正因为它完全跟着客户端走,所以它的值是不可控的,也是“不可信”的。一个恶意客户端完全可以往 Host 头里塞任何字符串,$http_host就会忠实地把这个字符串带进你的配置和日志里。如果你拿它做域名判断、路由分发,就必须警惕这种不可控性。

1.2 $host——经过 Nginx 规范化后的权威值

$host这个变量就聪明多了。它不是简单地取请求头,而是按照一个优先级规则,从几个来源里提取主机名。官方文档里写的规则是这样的:

  • 首先看请求行里有没有带主机名(HTTP/1.1 请求行通常是GET / HTTP/1.1,本身不含主机名,但 HTTP/1.0 请求行可能有);
  • 如果没有,就从请求头里的 Host 字段取;
  • 如果 Host 字段也没有,就用 Nginx 配置里匹配到的server_name。

很多老资料对$host的描述都停留在“取 Host 头”,其实在 Nginx 0.8.8 之后,$host的取值逻辑已经升级了:它会优先用当前请求匹配到的 server_name。也就是说,只要请求能匹配到某个 server 块的server_name,$host就优先等于这个server_name的值,而不是请求头里的 Host。

这一点差异非常关键。举个例子,你配置了server_name www.example.com example.com;,客户端虽然访问的是www.example.com,但它带的 Host 头可能是www.example.com,也可能是example.com,甚至可能是www.example.com:443这种带端口的形式。只要请求匹配到了这个 server 块,$host的值就会被 Nginx 统一规范成www.example.com或example.com(具体是哪一个是 Nginx 内部匹配到的那个)。与此同时,你会发现$host不带端口号,因为 server_name 本身就是不含端口的。

这样设计的好处很明显:你在用$host做逻辑判断、日志记录、转发规则时,拿到的总是一个“干净的、不带端口的主机名”,不会因为客户端写没写端口、写哪种格式而出现各种幺蛾子。

1.3 $proxy_host——upstream 上游服务器的主机名

$proxy_host和上面两个完全不同。它跟客户端请求头没有任何关系,它来自你配置的 upstream 或 proxy_pass 目标。当 Nginx 执行反向代理时,需要知道把请求转发到哪个后端地址,这个地址就是$proxy_host的值。

看一个最小示例:

upstream backend { server 10.0.0.3:8080; server 10.0.0.4:8080; } server { listen 80; server_name api.example.com; location / { proxy_pass http://backend; } }

在这个配置里,Nginx 做反向代理时,$proxy_host的值就是10.0.0.3:8080或10.0.0.4:8080(具体是哪一个取决于负载均衡算法选到了哪台机器)。如果 upstream 后面接的是域名,比如server api.internal.example.com:8080;,那$proxy_host就是api.internal.example.com:8080。

这里有个容易混淆的点:$proxy_host的英文名字里带个“proxy”,但它不是指“代理服务器自己的地址”,而是指“代理请求发往的目标地址”。我见过不少新手把它当成“当前 Nginx 的地址”来用,然后在proxy_set_header Host $proxy_host;里写出了匪夷所思的配置,把内网 IP 直接甩给了下游服务,导致后端虚拟主机全部匹配失败——就是我开头说的那个故障。

2. 组合实战:配合 proxy_pass 怎么选

单独认识三个变量是第一步,真正让你头疼的是在proxy_set_header里到底该填哪个。这一节我从实际配置出发,把每个选择的理由和后果讲明白。

2.1 一个最典型的反向代理配置

假设你有这样一个场景:用户访问https://blog.example.com,请求先到 Nginx,Nginx 再转发给内网的一台 Web 服务器192.168.1.10:8080,这台 Web 服务器上部署了多个虚拟主机,需要使用域名路由。

配置可以这样写:

server { listen 443 ssl; server_name blog.example.com; ssl_certificate /etc/nginx/ssl/blog.example.com.crt; ssl_certificate_key /etc/nginx/ssl/blog.example.com.key; location / { proxy_pass http://192.168.1.10:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } }

请求进来时,这三个变量分别是什么?我们实际“打印”一下。

在 Nginx 配置里加一段临时日志,输出这几个变量的值:

log_format test '$http_host | $host | $proxy_host'; access_log /var/log/nginx/test.log test;

然后发起一个标准请求:

curl -k https://blog.example.com/api/ping

日志里大概率会是这样:

blog.example.com | blog.example.com | 192.168.1.10:8080

三个值一目了然:$http_host和$host都是blog.example.com,因为 Host 头里没带端口;$proxy_host是192.168.1.10:8080,也就是 upstream 里的目标地址。

2.2 用 $host 还是 $http_host?要不要保留端口

上面那个例子里,$http_host和$host的结果碰巧一样,于是很多人就觉得无所谓。但一旦请求变成https://blog.example.com:443/api/ping,情况就完全不同了:

  • $http_host=blog.example.com:443
  • $host=blog.example.com
  • $proxy_host=192.168.1.10:8080

如果你用proxy_set_header Host $http_host;,那后端收到的 Host 就是blog.example.com:443。早期很多 Java 系的 Web 框架对带端口的 Host 处理得很粗糙,可能直接导致 session 或重定向 URL 出错。而用$host就干净多了,后端拿到的是blog.example.com,不会引发任何歧义。

所以我个人的经验是:默认情况下优先用$host,除非你真的需要把原始端口透传给后端。什么时候需要原样传?最常见的场景是后端做端口级路由,或者某些老应用强制校验 Host 头必须和浏览器地址栏完全一致。这种情况下你再用$http_host,而且要确保后端能正确解析带端口的 Host。

有人会说:$host不是也会受请求头影响吗?其实在大多数场景下,只要请求匹配到了 server 块,$host就会优先用 server_name,所以它对客户端自带 Host 的“免疫力”比$http_host强很多。但如果你的 server_name 写的是_或空,也就是所谓的“默认兜底服务器”,那$host就会退回用请求头里的 Host 值。这个细节我们在后面第三章详细讲。

2.3 什么时候一定要用 $proxy_host

$proxy_host最典型的用法,是你希望后端看到的主机名就是 upstream 里定义的那一个,而不是前端域名。

举个例子:你内网有一个服务registry.internal,配置了server registry.internal;。客户端通过公网域名hub.example.com访问 Nginx,你希望 Nginx 把请求转发给registry.internal,同时让后端服务认为用户在访问registry.internal而不是hub.example.com。这时候就可以写:

proxy_set_header Host $proxy_host;

这样后端收到的 Host 就是registry.internal(或者你 upstream 里写的registry.internal:8080,取决于定义)。

这个用法的典型坑我在开头提过:如果你在 upstream 里写的是 IP,比如server 10.0.0.3:8080;,然后proxy_set_header Host $proxy_host;,后端收到的 Host 就是10.0.0.3:8080。如果你的后端 Web 服务器是以域名做虚拟主机隔离的,那就全军覆没了,因为后端解析到的是一个它根本不认识的“主机名”。

所以,什么时候用$proxy_host,我总结了几条经验:

  • 后端服务明确要求 Host 必须等于它配置的 ServerName/域名时,用$proxy_host;
  • 后端是内网专用域名,不想暴露对外域名时,用$proxy_host;
  • 后端只认 IP、不认域名(某些内部接口),也要确保$proxy_host里写的是后端认识的 IP;
  • 大多数普通 Web 站点反代场景,用$host就够了。

3. 从几个特殊场景再深挖一遍

前面讲的是常规配置,但现实中总会遇到更拧巴的处境:没有匹配到 server_name、端口号去不掉、后端是 Unix Socket、TLS 握手阶段的 SNI 和 Host 头不一致。这些场景最能考验对变量的理解深度。

3.1 server_name 匹配不上的时候,$host 会怎样

先搞清楚 Nginx 选择 server 块的逻辑:当一个请求进来,Nginx 会先根据端口找到监听该端口的 server 块,然后在这些 server 块里按照 server_name 匹配。匹配顺序大概是:精确匹配 > 通配符前匹配(*.example.com)> 通配符后匹配(www.example.*)> 正则匹配 > 默认 server。

如果你绑定了server_name example.com;,但客户端请求的 Host 是wrong.example.com,这时候 Nginx 匹配不到精确的 server_name,就会落到listen 80 default_server;那个块上。如果默认 server 块里没有显式写 server_name(或者写的_),那么$host会发生什么?

答案是:$host会退回去用请求头里的 Host 值,也就是wrong.example.com。这时候你会发现$host和$http_host又变得完全一样了。

这个行为在实际运维中很迷惑人。我遇到过一种典型情况:默认 server 块里配置了proxy_set_header Host $host;,结果所有未匹配域名都带着原始 Host 打到了后端,谁也不知道自己访问的其实是一个兜底站点。如果你希望默认 server 里能“强行纠正” Host,就得在配置里写死,或者用 map 做规则转换。

更隐蔽的坑是:当 Host 头缺失时(HTTP/1.0 客户端),$http_host为空,$host则取 server_name。这时候日志里如果一个字段有空值,就要立刻想到可能是旧协议客户端。本来 Nginx 会直接对缺 Host 的 HTTP/1.1 请求返回 400,但 HTTP/1.0 是允许不带 Host 的,很多老监控脚本就是这么触发的。

3.2 端口、IPv6 与 Unix Socket 场景

端口问题我在 2.2 已经说了一部分,这里补充一个更刁钻的情况:如果$host是从 Host 头里取的,那它其实也可能带端口。比如默认 server 块里server_name _;,客户端请求example.com:8080,由于没有匹配到具体的 server_name,$host从 Host 头取值,就会变成example.com:8080。也就是说,$host不带端口这件事并不是绝对的,只有在走 server_name 取值时才确定不带端口。

再来看 IPv6。如果 upstream 里写的是 IPv6 地址:

upstream backend { server [::1]:8080; }

$proxy_host的值就是[::1]:8080。如果你把$proxy_host塞进 Host 头发给后端,那后端就会收到一个奇形怪状的 Host。这种场景我还真见过——有人从旧配置里复制了一段proxy_set_header Host $proxy_host;,而旧配置写的恰好是 IPv6 地址,结果新环境后端一直 400。问题排查了很久才发现,不是后端配置错,是 Host 头里带了[。

Unix Socket 场景更特殊。当proxy_pass http://unix:/tmp/backend.sock;时,$proxy_host的值会是 socket 文件路径/tmp/backend.sock,这种值拿去当 Host 头简直是灾难。所以一定记住:$proxy_host的设计初衷是用来生成连接到上游的地址,不是用来传递 Host 头的。绝大多数情况下,你把$proxy_host打出去,都是你不想要的。

3.3 SNI 与 Host 头:TLS 层面的另一套逻辑

TLS 握手时有一个扩展叫 SNI(Server Name Indication),客户端会在握手阶段告诉服务器“我要访问的是哪个域名”,服务器据此返回对应的证书。这个阶段,HTTP 层的 Host 头还没出现呢——因为 HTTP 请求是在 TLS 握手完成之后才发的。

Nginx 里对应的变量是$ssl_server_name,它记录的是 SNI 里的域名。如果你在做多域名 SSL 证书方案(SNI 分流),会发现$ssl_server_name和$host在绝大多数正常浏览器请求下是一致的,但两者不是一回事。恶意客户端完全可以构造 SNI 为A.com、Host 头为B.com的请求。

这跟我们的三个变量有什么关系?关系很大。当你用$host做路由,而 Nginx 在 TLS 层用$ssl_server_name选证书时,两者一旦对不上,就可能出现“HTTPS 证书是 A 的,但转发后端时 Host 是 B”的情况。排查这类问题,不要把目光只锁定在 HTTP 层,要同时对比$host和$ssl_server_name。

另外要提一句 SSL 证书替换不生效的经典问题。很多人换完证书发现还是旧证书,其实不是证书文件没更新,而是客户端连的域名没变,但 SNI 可能命中了 Nginx 缓存,或浏览器还在用 TLS 会话复用。排查时最简单的办法是用openssl s_client -connect 域名:443 -servername 域名强制新建握手,看返回的证书链是否是新的。这和$ssl_server_name的关系就在于:如果你 SNI 指向的域名不是证书上写的那个,即使证书文件换得再对,也会报“证书不匹配”。所以,换证书不生效时,先确认 SNI 域名、$host、证书 CN/SAN 三者是否一致。

4. 常见问题排查与避坑速查

最后这节是实战排查笔记。所有问题均来自我自己的线上经历和社区里高频出现的求助帖,按“现象 -> 原因 -> 解决办法”的格式整理,方便你直接对照。

4.1 后端日志出现 400 Bad Request

现象:Nginx 报 200,但后端应用日志一堆 400,或者反向代理直接返回 400。

原因:HTTP/1.1 强制要求请求必须携带合法的 Host 头。如果你在proxy_set_header里写了Host $http_host;,而客户端恰好没传 Host(比如某些 HTTP 客户端库的 bug),$http_host为空,Nginx 就会把空 Host 转发给后端,后端直接 400。另一种情况是proxy_pass后面跟了变量,导致 Nginx 无法在配置加载阶段判断 Host 该填什么,于是选择不填充默认头部,或把$proxy_host填进去,后端不认。

解决办法:不要裸用$http_host作为 Host,优先$host。如果业务确实需要原样 Host,至少加一层判断:

set $forwarded_host $host; if ($http_host != "") { set $forwarded_host $http_host; } proxy_set_header Host $forwarded_host;

这种写法能保证 Host 头非空,同时尽量保留原始值。

4.2 后端总是拿到“内网 IP:端口”这种 Host

现象:后端日志里 Host 全是类似10.0.0.3:8080的内网地址,虚拟主机全部匹配到默认站点。

原因:配置里写了proxy_set_header Host $proxy_host;,而$proxy_host的值就是 upstream 定义的内网 IP 端口。

解决办法:把Host改成$host(对外域名)或者后端真正期望的域名。如果你既不想对外暴露域名,又想让后端识别特定站点,就在 upstream 里给后端定义域名解析,比如server backend.internal.example.com:8080;,这样$proxy_host至少是个可读域名。但最稳妥的做法还是显式写死后端期望的 Host:

proxy_set_header Host "backend.internal.example.com";

4.3 日志里的域名总是带着端口,需要去端口

现象:访问日志里$http_host输出成example.com:443或example.com:8080,分析时很恼人;或者后端应用在生成重定向链接时把:443拼进去,导致跳转 URL 异常。

原因:客户端在 Host 头里带了端口,而你用的是$http_host原样记录/转发。

解决办法:日志分析用$host替代$http_host,因为$host在正常 server_name 匹配场景下是去端口的。转发场景同理,把proxy_set_header Host改为$host。如果遇到 3.2 说的那种默认 server 场景($host退化为 Host 头),就用 map 强制剥离端口:

map $http_host $clean_host { ~^(?P<h>[^:]+)(:\d+)?$ $h; default $http_host; }

然后把Host $clean_host;用上。

4.4 三个变量怎么选:速查表

目标推荐变量理由
记录客户端原始访问域名(含端口)$http_host原样不动
记录/传递规范化域名(推荐默认)$host优先 server_name,去端口
强制后端保留原始端口$http_host唯一保留端口的选项
让后端看到 upstream 定义的地址$proxy_host上游定义即可
日志里展示前端用户访问的域名$host稳定、干净
默认 server 里做兜底判断$host至少取到 Host 或 server_name
TLS 层判断域名$ssl_server_name(配合)握手阶段专用

4.5 一些容易被忽视的隐形坑

proxy_set_header Host $proxy_host;的坑容易理解,但下面几个隐形场景,我敢说不少人都没注意到:

第一个坑:多个 location 共享上游连接时,$proxy_host可能和你想象的不一样。如果你在某个 location 里直接写了proxy_pass http://192.168.1.10:8080;,那么$proxy_host就是192.168.1.10:8080。但如果你写的是proxy_pass http://$backend_url;(变量形式),Nginx 无法在解析配置阶段确定上游地址,$proxy_host的取值就会变得很不可控,某些老版本甚至直接保持未初始化。这属于超级冷门但真实存在的坑。能不用变量写 proxy_pass 就别用,除非你完全清楚影响。

第二个坑:$http_host和 HTTPS 下的 443 端口。很多人在 HTTPS 站点里用$http_host记录日志,结果所有访问记录里都带着:443,又长又丑。更麻烦的是,某些后端框架看到:443会在重定向时拼出不带端口或被浏览器拦截的 URL。与其事后写正则清洗,不如一开始就用$host。

第三个坑:proxy_set_header Host $host;配上了但没生效。这种情况往往是因为你proxy_set_header Host写在了location外面,而location内部又有其他配置覆盖了它;也可能是 upstream 里设置了keepalive,连接复用后头部没有按预期更新。遇到这种“配置明明写了却没生效”的情况,第一步是用nginx -T导出实际生效的完整配置,看看最终的 Host 指令到底落在哪个层级。

第四个坑:默认 server 块的反向代理,$host可能带端口。我在 3.2 里说过,当没有 server_name 可匹配时,$host退化。很多监控系统只统计域名不匹配的请求,如果用$http_host或者退化的$host,统计结果会失真。建议默认 server 里用 map 或正则做一层规范化,或者干脆在默认 server 的日志格式里固定只记录原始 URI。

就我个人的使用习惯而言,现在写新配置时,除非遇到明确的端口透传需求,否则一律proxy_set_header Host $host;,日志一律用$host做域名统计,$http_host只在调试阶段用来确认“客户端到底发了什么”。这个习惯帮我少踩了很多坑,也推荐你尝试。最后再提醒一句:别把$proxy_host塞进 Host 头,除非你真的很清楚自己在干什么——那是我踩过最深的坑,没有之一。

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

只有文案怎么自动生成短视频?5款文生视频工具实测横评

只有文案怎么自动生成短视频&#xff1f;这是很多矩阵运营和个人创作者在起步时遇到的第一个工程问题。文生视频&#xff08;Text-to-Video&#xff09;是指通过输入自然语言提示词或分镜脚本&#xff0c;由 AI 模型直接生成连续动态画面的技术。对于需要高频产出的团队&#x…

作者头像 李华
网站建设 2026/10/1 15:14:33

树上差分与LCA:从“闇の連鎖”理解边差分模型

看到“闇の連鎖”这个标题&#xff0c;我第一反应是哪个番的剧情&#xff0c;直到打开题面才发现&#xff1a;这是经典的树上差分模型题&#xff0c;考察的就是边差分 dfs预处理这套组合拳。题目结构很简洁——n 个点&#xff0c;n-1 条主边构成一棵树&#xff0c;另外再给 m …

作者头像 李华
网站建设 2026/10/1 15:13:44

Codex CLI 配 TaoToken:AI编程智能体 settings.json 骨架与实战验证

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

作者头像 李华