ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Nginx三个Host变量详解:$host、$http_host与$proxy_host的区别

Nginx三个Host变量详解:$host、$http_host与$proxy_host的区别 做Nginx配置做得久了很难绕开$http_host、$host、$proxy_host这三个变量。尤其是刚开始写反向代理、多域名跳转、SSL强制转发的时候很多人会想当然地觉得“既然都是 Host那应该差不多吧”结果就是循环重定向、后端收到错误域名、访问控制失效、WebSocket握手失败……各种奇怪问题挨个冒出来。这篇文章不打算只给一张“区别表”就收工而是把这三个变量从底层取值逻辑到实际配置场景全部拆开结合我踩过的坑和排查思路一次性讲清楚。如果你是刚接触Nginx没多久的新手这篇文章能帮你少走很多弯路如果你已经写过不少配置也可以当成一份查漏补缺的备忘录。核心就一句话$http_host是客户端的原话$host是整理过的主机名$proxy_host是Nginx打算转发给后端的那台机器地址三者各管一段绝对不能糊着用。1. 先还原一个几乎所有Nginx使用者都遇到过的现场1.1 配置看起来没问题访问却跳到了默认站点我有一次在服务器上部署了三个站点配置大概长这样server { listen 80; server_name a.example.com; root /var/www/a; } server { listen 80; server_name b.example.com; root /var/www/b; } server { listen 80 default_server; server_name _; root /var/www/default; }按道理访问a.example.com应该落到第一个server块访问b.example.com落到第二个。可是实际测试时发现只要请求头里的Host写得不太“规范”比如curl -H Host: a.example.com:8080 http://你的IP/流量就会直接掉进default_server甚至被判定成未知站点。当时我一度怀疑是DNS解析问题后来在Nginx的access_log里把$host和$http_host打出来才发现两个变量值根本不一样一个是a.example.com另一个是a.example.com:8080。这个现象背后隐藏的正是三个变量最核心的区别Nginx在做server_name匹配时会先对Host头做一次“整理”端口会被剥掉然后拿去和配置里的server_name比较但如果你在日志、rewrite、proxy_set_header里直接引用了$http_host拿到的是客户端请求里未经处理的原始值端口还在甚至会保留原始大小写。1.2 日志里的差异同一个请求三个变量的值不一样我特别喜欢用一个调试接口来“肉眼观察”这三个变量。在任意server块里加一段location /debug_host { default_type text/plain; return 200 host$host\nhttp_host$http_host\nproxy_host$proxy_host\n; }然后用不同方式发起请求看到的结果会非常直观。假设我的Nginx是192.168.1.10反向代理后端是10.0.0.5:9000请求是curl -H Host: www.example.com:8080 http://192.168.1.10/debug_host在不经过proxy_pass的普通location里大致会得到hostwww.example.com http_hostwww.example.com:8080 proxy_host一旦访问的location里配置了proxy_pass http://10.0.0.5:9000;并且在proxy_set_header Host $proxy_host;后端应用收到的Host就会变成10.0.0.5:9000而不再是客户端传来的www.example.com:8080。也就是说$proxy_host根本不是从请求里取出来的它是Nginx根据proxy_pass里写的目标地址自动生成的。同一个请求三个变量三个含义理解到这一步后面所有配置决策都不会跑偏。2. 变量取值规则拆解Nginx究竟把哪些值塞进了这三个变量2.1 $http_host请求头Host的“原样搬运工”$http_host属于Nginx的“请求头系列变量”。只要客户端在HTTP请求里带了Host头这个变量就等价于那个Host头的完整值多一个字符都不会改。HTTP/1.1协议要求客户端必须携带Host头用来告诉服务器“我这次想访问的是哪个域名”。所以绝大多数现代请求里$http_host都不会为空。但这不代表你可以完全依赖它因为还有三类情况会让它变得不可靠老旧的HTTP/1.0客户端可能不带Host头有些压测工具、脚本、端口扫描器会故意不写Host头客户端可以随便写写一个和你的站点毫无关系的域名甚至写一个带注入风险的恶意值。$http_host的特点就是“忠实记录”端口可能带着大小写可能带着非法字符也可能带着。它最适合用在你确实需要“原样传递”的场景最不适合用在需要“规范化判断”的场景。2.2 $host最常用的“规范主机名”$host和$http_host最大的区别是Nginx会对这个值做“清洁处理”。按照官方文档和源码逻辑它的取值优先级大致是这样的如果请求行里带了主机名例如HTTP/2的:authority优先使用请求行的值否则取请求头Host字段的值如果连Host头都没有比如HTTP/1.0请求就使用当前处理这个请求的server块里配置的server_name。拿到值之后Nginx还会做两件事转成小写去掉端口。所以$http_host是www.Example.com:8080时$host很可能是www.example.com。这个“整理过的非端口域名”非常适合做301/302跳转、域名白名单、基于域名的路由分发因为业务逻辑通常只关心“哪个域名”不关心“客户端从哪个端口过来的”。2.3 $proxy_host只有代理转发时才出现的“上游地址”$proxy_host的存在感和前两个完全不一样。它不是从客户端请求里取的而是Nginx在处理proxy_pass时自动生成出来的一个变量代表的是上游服务器地址也就是proxy_pass配置里的host和端口。举几个例子proxy_pass http://backend:8080;这里$proxy_host的值就是backend:8080。proxy_pass http://192.168.1.20;如果proxy_pass没有写端口$proxy_host会根据协议补上默认端口比如HTTP默认80HTTPS默认443所以这里就是192.168.1.20。还有一个容易忽略的点在没有配置proxy_pass的普通server块或location块里$proxy_host是空字符串。它也拿不到“客户端原本想访问的域名”。所以千万不要试图用$proxy_host来替代$host做域名判断那是完全错误的。为什么Nginx要专门生成一个$proxy_host因为在代理转发时Nginx需要决定“我用什么Host头去访问后端”。如果不手动设置proxy_set_header HostNginx默认使用的就是$proxy_host也就是向后端暴露“代理服务器的目标地址”而不是客户端的原始域名。很多后端应用恰恰因为这个默认行为拿到了一堆127.0.0.1:9000或者内网服务名导致生成回调链接时全部出错。2.4 一张表看清三者来源与应用位置为了方便对照我整理了一张表几乎覆盖了日常使用中所有关键差异变量数据来源是否保留端口是否转小写在反向代理中代表什么典型用途$http_host客户端请求头Host是否客户端原本发送的原始Host头原样透传、调试、透传自定义端口$host请求行/请求头/当前server_name否是经过Nginx规范化后的主机名跳转、域名白名单、路由分发$proxy_hostproxy_pass配置中的目标地址是按配置中写法上游服务器地址后端需要识别上游时使用这张表看明白之后再去套用下面的配置场景思路会清晰很多。3. 怎么选才不出事重定向、反代、SSL、访问控制的分场景决策3.1 做301/302跳转时优先用$host而不是$http_host我见过很多朋友写“HTTP强制跳转HTTPS”时用的是这种配置server { listen 80; server_name example.com www.example.com; return 301 https://$http_host$request_uri; }乍一看好像没问题实际上坑很多。第一如果用户是用http://example.com:8080访问的$http_host会把:8080拼进跳转URL最后生成https://example.com:8080/xxx。你这个HTTPS站点如果只监听443就会造成跳转后无法访问。第二如果客户端传的Host头大小写比较随意跳转后的域名也会出现大小写不一致虽然影响不大但观感很糟糕。更稳妥的写法是server { listen 80; server_name example.com www.example.com; return 301 https://$host$request_uri; }$host把端口剥掉了只保留规范主机名。$request_uri则保留了原始URI和查询参数这样拼出来的跳转地址最干净。那什么时候才需要用$http_host做跳转只有一种情况你的服务就依赖非标准端口并且跳转时必须保留这个端口。比如内部某个服务跑在http://service.internal:18080你想让80端口访问时跳到18080return 301 http://$http_host:18080$request_uri;但这种场景非常少见。绝大多数对外站点用$host都是更安全的选择。3.2 反向代理后端真正需要的是哪个Host反向代理是三个变量最容易“同框”的场景也是坑最多的地方。关键在于你要想清楚后端应用期望收到什么Host。第一种情况后端是一个普通的Web应用比如Tomcat、Node.js它需要知道用户是通过哪个域名访问的才能生成绝对链接或者做域名维度的权限判断。此时你应该用$hostlocation / { proxy_pass http://app_upstream; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; }这样后端收到的是example.com干净且规范。但如果用户访问时带了非标准端口比如http://example.com:8080这里用$host会把端口丢掉。后端如果要生成“当前页面完整URL”端口信息就丢了。这时候你可以考虑用$http_host或者额外传一个X-Forwarded-Host头让后端自己决定怎么处理。第二种情况后端是内网服务它不关心客户端域名只认上游地址。例如Nginx把请求转发到内网的order-server:8080而这个服务配置了“只允许来自order-server:8080的Host头”那你就用$proxy_hostlocation /order/ { proxy_pass http://order-server:8080; proxy_set_header Host $proxy_host; }这时候后端收到的Host就是order-server:8080完美匹配它的内部逻辑。第三种情况后端需要拿到客户端的原始Host包括端口和大小写那就直接配置proxy_set_header Host $http_host;我自己的经验是能用$host就用$host除非明确需要保留端口或者后端有特殊校验。因为$http_host完全信任客户端的输入万一有人故意传一个畸形Host后端的解析逻辑如果不够健壮很容易出问题。3.3 HTTPS、自签名证书与端口陷阱配置HTTPS时很多人会顺手在跳转和HSTS里用$host这个方向是对的但有几个边界值得注意。一个是监听443的server块里如果同一台服务器上有多张证书你需要根据$host动态选择证书。虽然Nginx原生ssl_certificate指令不能在if或location里动态生效但你可以通过“多个server块分别指定server_name和证书”来实现这是最常规的玩法。比如server { listen 443 ssl; server_name a.example.com; ssl_certificate /etc/nginx/ssl/a.example.com.crt; ssl_certificate_key /etc/nginx/ssl/a.example.com.key; } server { listen 443 ssl default_server; server_name b.example.com; ssl_certificate /etc/nginx/ssl/b.example.com.crt; ssl_certificate_key /etc/nginx/ssl/b.example.com.key; }只要客户端用https://a.example.com访问Nginx在TLS握手阶段就会根据SNI选出对应证书然后进入对应server块此时$host就是a.example.com。另一个是自签名证书调试时很多人喜欢用IP加端口访问这时候$host是IP$http_host是IP:端口。如果在HSTS头里用了$http_host可能会把端口也塞进Strict-Transport-Security虽然大多数浏览器会忽略但规范上Absolutely不推荐。HSTS头应该只写域名所以正确做法是add_header Strict-Transport-Security max-age31536000; includeSubDomains always;不要手动拼接变量省心很多。3.4 鉴权与空Host为什么安全过滤要用$host做域名白名单或防踩踏过滤时很多人会写出这样的判断if ($http_host !~* ^(example\.com|www\.example\.com)$) { return 403; }这个写法有个隐患对于不带Host头的HTTP/1.0请求$http_host是空字符串空字符串固然匹配不上正则最终会返回403看起来没什么问题。但如果你写的是“放行名单”逻辑比如if ($http_host ~* ^(example\.com)$) { # 放行 }空Host头就会直接绕过判定进去。换成$host就安全很多因为当请求头没有Host时Nginx会用当前server块的server_name来填充$host这个值是你自己配置的不可能为空或不可控。if ($host !~* ^(example\.com|www\.example\.com)$) { return 444; }444是Nginx特有的返回码会直接断开连接不返回任何响应体适合拿来挡掉不认识的域名或恶意扫描。4. 可直接落地的三套配置模板4.1 HTTP到HTTPS规范跳转最常用的“全站强制HTTPS”配置模板我建议这样写server { listen 80; listen [::]:80; server_name example.com www.example.com; # 非HTTPS请求一律跳走 if ($host !~* ^(example\.com|www\.example\.com)$) { return 444; } return 301 https://$host$request_uri; }关键点在于server_name只列你真正允许的域名。其他域名访问80端口时要么会落到别的server块要么会走默认站点。跳转时用$host而不是$http_host避免端口残留。如果你还有个“任意域名访问都跳到主站”的需求可以改成return 301 https://example.com$request_uri;这时候不管客户端传什么Host最终都会统一到example.com对SEO和账号体系统一有好处。4.2 基于$host的多站点复用与多证书选择很多人在单台服务器上用同一个Nginx部署多个应用手动复制一份server块当然最直接但如果你希望配置更集中可以借助map把域名映射到不同后端。map $host $app_backend { hostnames; app1.example.com http://app1:8080; app2.example.com http://app2:8080; default http://default_app:8080; } server { listen 80; server_name app1.example.com app2.example.com; location / { proxy_pass $app_backend; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }注意proxy_pass后面如果直接使用变量Nginx会认为这是一个“动态解析的upstream地址”此时如果你不显式设置proxy_set_header Host默认的$proxy_host可能不是你预期的值。所以在所有变量形式proxy_pass的配置里都建议手动指定Host头最保险的就是$host。另外如果你在同一server块里对不同域名要走不同证书用map是做不到的。证书选择必须在server块级别通过server_name区分这一点不要试图用变量去挑战Nginx的模块边界。4.3 反代WebSocket和长连接时Host头怎么设置WebSocket握手和普通HTTP不太一样客户端发升级请求时Host头必须和后端期望的域名一致否则后端会认为来源非法。我在代理FreeSWITCH的WebSocket端口时踩过一个大坑后端配置了基于域名的校验但Nginx转发时默认用了$proxy_host把Host头变成了内网地址导致握手一直失败。正确的WebSocket反代配置如下location /ws/ { proxy_pass http://ws_backend; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection upgrade; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; }这里用$host把客户端访问的域名传给后端。如果后端还要求保留原始端口可以考虑$http_host但WebSocket场景一般走标准80/443端口$host足够。如果Nginx和上游之间要使用keepalive长连接同样要处理好proxy_set_header Host否则连接池里的请求可能会串Host。4.4 容器/内网环境Docker里Nginx和上游服务别名Docker Compose里部署Nginx和业务容器时我经常看到这样的配置location /api/ { proxy_pass http://backend:8000; proxy_set_header Host $proxy_host; }后端容器收到Host是backend:8000而业务代码里如果做了域名白名单比如必须等于api.example.com就会直接拒绝请求。更合理的是location /api/ { proxy_pass http://backend:8000; proxy_set_header Host $host; }这样后端收到的就是客户端实际访问的域名。你的前端如果通过https://api.example.com/api/xxx访问这个头就是api.example.com完全匹配。如果后端确实需要靠“服务名”来做内部路由再考虑$proxy_host。判断标准只有一个后端代码希望看到“外部域名”还是“内部服务名”。5. 排查手段与个人经验怎么快速判断该用哪个5.1 用日志和调试接口打印三个变量排查这类问题最快的方式是让Nginx亲口告诉你答案。除了前面提到的return 200调试法你还可以在access_log里加上自定义字段把每个请求的三个变量都记录下来log_format main $remote_addr $request host$host http_host$http_host proxy_host$proxy_host upstream_addr$upstream_addr;然后access_log /var/log/nginx/access.log main;重载Nginx后找一个测试请求看日志所有信息一目了然。例如192.168.1.100 GET /api/user HTTP/1.1 hostapi.example.com http_hostapi.example.com proxy_host127.0.0.1:8080 upstream_addr127.0.0.1:8080如果proxy_host和你预期的不一样多半是proxy_pass写法和想象中不一致如果host变成了IP或者_大部分情况是客户端没有带Host头或者当前server块是default_server。5.2 实测场景CDN回源时为什么$http_host携带了回源端口有个真实案例站点套了CDNCDN回源到源站Nginx时默认会把客户端原始Host头一起带回来有时候还会在Host后面追加回源端口。结果源站做301跳转时用了$http_host生成了一堆带奇怪端口的URL用户点开后直接打不开。解决办法就是跳转一律用$host不要用$http_host。如果你需要在源站判断“原始请求来自哪个域名”用$host就够了如果你还关心“客户端原本从哪个端口访问”建议让CDN额外传一个X-Forwarded-Port头而不是去解析$http_host的端口。因为$http_host并不是总能代表客户端的原始端口它可能是CDN的某个内部回源端口非常容易误导。5.3 实测场景server_name带通配符与正则时的“意外”另一个容易“意外”的地方是server_name里带通配符或正则。比如你写了server { listen 80; server_name *.example.com; return 200 $host; }客户端请求foo.example.com时$host返回的是foo.example.com这点大家都好理解。但如果你用了正则比如server_name ~^www\.(?domain.)$;Nginx会尝试给正则里的命名捕获赋变量$host仍然是“整理后的请求Host”而不是捕获的部分。很多人会把$host和正则捕获混在一起导致判断逻辑出错。实际写业务时要区分清楚$host永远只表示当前请求的主机名与server_name内部如何匹配没有直接关系。当然当请求头里没有Host时$host会使用当前server块的server_name。如果这个server_name是_那$host就是_这也是一个比较常见的“坑”。所以做严格域名判断时最好把_也当作非法值处理或者用default_server统一拦截。5.4 排查流程总结问自己三个问题遇到和Host相关的诡异问题我通常不急着改配置而是先问三个问题这个变量是来自客户端请求还是来自Nginx配置来自客户端的是$http_host经过整理的是$host来自上游配置的是$proxy_host。接收方希望看到的是原始域名、规范化域名还是上游地址对浏览器、用户跳转、业务域名判断用$host对需要保留原始端口或原始大小写的透传用$http_host对后端内部服务识别用$proxy_host。这个变量会不会为空或含意外端口如果可能为空就优先用$host如果可能带端口就仔细想清楚端口到底该不该保留。把这三个问题过一遍绝大多数Host相关的配置错误都能在动手改之前就发现。最后再分享一个我自己的习惯每次新建站点或反向代理配置我都会先加一个临时的调试接口或者专门记录变量到日志跑一两个测试请求确认实际值再正式提交配置。别嫌这一步麻烦三个变量好比三把钥匙用错了锁打不开问题却不会写在报错里只能靠日志和调试把真相挖出来。这些经验都是踩坑踩出来的希望你看完这篇之后下次遇到Host相关的问题能少折腾几个小时。
返回列表