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

发布时间:2026/10/1 15:17:00
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请求先到 NginxNginx 再转发给内网的一台 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_hostblog.example.com:443$hostblog.example.com$proxy_host192.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 握手时有一个扩展叫 SNIServer 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至少是个可读域名。但最稳妥的做法还是显式写死后端期望的 Hostproxy_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 { ~^(?Ph[^:])(:\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_nameTLS 层判断域名$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 头除非你真的很清楚自己在干什么——那是我踩过最深的坑没有之一。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询