Nginx server_name匹配规则、常见坑与配置模板
先说个真实的场景。上个月帮一个朋友排查线上问题他们新上的一个站点用户访问主域名时偶尔会看到另一个项目的首页刷新几次又恢复正常。查了半天最后发现罪魁祸首就是 Nginx 里的 server_name 指令写错了——一个配置块里少了两个域名另一个配置块里的正则写得太宽把本该属于自己的请求抢走了。这种问题太典型了。server_name 在 Nginx 里看起来就是一个填域名的地方但它的匹配规则、优先级、和 listen 指令的协作方式稍不注意就会埋雷。很多人配置了几年 Nginx遇到多域名、泛解析、默认站点这类需求时还是靠试错试错了也不知道为什么错。这篇文章就围绕 server_name 这个配置指令把它的原理、匹配规则、高频坑位和可复用的配置模板完整过一遍希望能帮你少走弯路。1. server_name 的真实作用不是设置域名而是请求分发很多人对 server_name 的理解停留在表面写在某个 server 块里代表这个站点对应的域名。这个理解不能算错但远不够准确。真正搞清楚它得先明白 Nginx 处理一个 HTTP 请求时做了什么决策。1.1 从一次域名指向错误的排障说起假设你的服务器上有两个站点一个绑定www.example.com另一个绑定api.example.com两个站点的 Nginx 配置都监听 80 端口server { listen 80; server_name www.example.com; root /var/www/www; } server { listen 80; server_name api.example.com; root /var/www/api; }当浏览器请求http://www.example.com时请求到达 Nginx 的 80 端口Nginx 会先看这个请求的 Host 头是什么。HTTP/1.1 协议要求请求必须携带 Host 头它的值就是用户输入的那串域名。Nginx 拿到 Host 头为www.example.com然后去和两个 server 块里的 server_name 比较发现第一个匹配于是把请求交给第一个 server 块处理。这个过程本质上跟酒店前台按客人姓名查房卡是一样的。listen 指令决定了你走进哪个酒店大门哪个 IP端口server_name 决定了前台把你分配给哪间房哪个虚拟主机。两者分工明确缺一不可。1.2 listen 和 server_name 的分工边界有一个常见的误解以为 listen 相同的两个 server 块server_name 必须不同否则会冲突。真相是可以相同甚至可以完全重复。Nginx 允许这种情况存在只是匹配时永远只挑第一个符合条件的 server 块来处理。反过来如果你只配置了 listen 80没写 server_name那么这个 server 块会成为所有访问 80 端口请求的默认处理者——不管 Host 是什么只要端口对上就归它管。这种什么都不挑的特性在某些场景很有用但也容易造成服务混乱后面我会专门讲到。所以记住一句话listen 决定请求能不能进这个门server_name 决定进门之后归谁管。理解这个边界是避免配置混乱的第一步。1.3 匹配不到会发生什么如果你配置了三个 server 块每个都有不同的 server_name而用户请求的 Host 头恰好哪个都没匹配上Nginx 不会报错而是有一个兜底逻辑在相同 listen 地址和端口的 server 块中如果没有显式指定 default_server就选配置文件里第一个出现的 server 块来响应。这个兜底逻辑在实践中产生过很多诡异现象明明访问的是 A 域名却返回了 B 站点的内容可能就是因为你没设默认服务器而 B 站点恰好排在配置文件的第一个位置。2. server_name 的匹配优先级精确 通配符 正则别被顺序骗了server_name 支持三种写法精确名称、通配符名称、正则表达式。Nginx 在匹配时有一套严格优先级很多人栽在这里是因为以为正则写在前面就能优先匹配实际上不是的。2.1 三类匹配写法及官方优先级排序官方文档定义的优先级从高到低是精确匹配完全相等的字符串以*开头的最长通配符如*.example.org以*结尾的最长通配符如mail.*正则表达式按配置文件中的书写顺序第一个匹配的生效注意第 2 和第 3 都写了最长这是通配符内部的比较规则通配符部分匹配到的字符越多优先级越高。比如*.example.org和*.sub.example.org同时存在时后者更长优先匹配a.sub.example.org这类请求。这个优先级排序不受配置顺序影响。也就是说哪怕你把一个正则在配置文件的最前面而某个请求既能被精确匹配命中也能被正则命中最终也会走精确匹配。2.2 一个反直觉的实测案例我自己做过一次测试配置如下server { listen 80; server_name ~^.*\.example\.com$; return 200 regex hit; } server { listen 80; server_name www.example.com; return 200 exact hit; }按理说正则写在前面但请求www.example.com的结果是exact hit。这验证了优先级的绝对性。在排查问题时如果你发现某个正则永远不生效先别急着改写法看看是不是有精确匹配或通配符匹配抢在前面。2.3 通配符的两个隐藏限制通配符的规则比大多数人的直觉严格*.example.org能匹配a.example.org、a.b.example.org支持跨多级子域但绝不匹配裸域example.org本身。如果你希望裸域和子域都指向同一站点必须显式写两行server_name example.org *.example.org;通配符只能出现在名称的开头或结尾不能写在中间。像www.*.example.org这种写法是无效的Nginx 会报配置错误。有人觉得通配符既然能跨多级为什么不能匹配裸域因为裸域和子域在 DNS 语义上就是两个不同的 entityNginx 的匹配是字符串层面的规则不做 DNS 层面的包含判断。2.4 正则写法的注意事项正则匹配以~开头表示区分大小写~*开头表示不区分大小写。官方推荐在 server_name 中尽量使用~^锚定开头否则可能产生意外匹配。比如server_name ~^www\.example\.(com|org)$;这里点号必须转义为\.因为正的点号在正则里匹配任意字符。我见过有人把www.example.com写成~^www.example.com$结果wwwXexampleXcom这种诡异的 Host 也能命中这在公网环境中是一颗不定时炸弹。正则还有一个特性可以使用命名捕获组捕获的值会成为变量供后续指令引用server_name ~^(?subdomain.)\.example\.com$;这样$subdomain可以在该 server 块内直接被使用比如做基于子域名的动态 root 或反向代理。这个功能很实用但不要滥用正则写复杂了维护成本很高。3. 高频翻车场景五个真实踩坑案例与正确姿势原理讲完了再看实际场景里最容易翻车的地方。以下五个坑都是我踩过、或者帮别人排查过的每个都有代表性。3.1 用 IP 访问时走进了错误的站点情况服务器上有多个 server 块都监听 80 端口。用户直接用http://服务器IP访问结果看到的是 A 站点而大多数人的预期是应该看到默认站点或者被拒绝。这个现象的根本原因是用 IP 访问时HTTP 请求的 Host 头是 IP 地址本身或其他形式。Nginx 拿这个值和所有 server_name 匹配通常一个都匹配不上于是触发了兜底逻辑——选第一个 server 块。解决方式有两种。第一在其中一个 server 块中显式加default_server参数server { listen 80 default_server; server_name _; return 444; }这样所有匹配不到 host 的请求都会走进这个块return 444表示直接关闭连接不给任何响应。第二把所有已知域名都写在各自的 server_name 里让未知 Host必须走默认块。这里顺带说一句server_name _;里的下划线并不是 Nginx 的特殊语法只是一个永远不会真正匹配到合法域名的占位符。它的作用就是让这个 server 块只可能通过 default_server 被选中语义上表达我是兜底。3.2 主域名和 www 之间的跳转回环电商站常见的需求用户访问example.com要 301 跳转到www.example.com。很多人的配置长这样server { listen 80; server_name example.com; return 301 http://www.example.com$request_uri; } server { listen 80; server_name www.example.com; root /var/www/site; }这套配置看着没问题但在 HTTPS 场景下特别容易出回环如果用户访问https://example.com80 端口的跳转块根本不接收 443 流量而 443 的站点块又没处理example.com这个域名用户就会看到证书错误或站点拒绝连接。更合理的做法是让两个域名共用一个 server 块server { listen 80; listen 443 ssl; server_name example.com www.example.com; if ($host example.com) { return 301 https://www.example.com$request_uri; } }这里用if判断当前 Host只在访问裸域时跳转避免了对www域名也做跳转造成的循环。3.3 通配符和正则的混淆使用有人为了匹配所有以某个域名结尾的请求写了server_name .example.com;实际上.example.com是*.example.com的简写形式它同样不匹配裸域example.com。如果你本意是所有 example.com 相关的请求都归这里管正确的写法是server_name example.com .example.com;还有另一种混淆把正则的.*当成通配符用。比如server_name ~^.*\.example\.com$;这个正则能匹配a.example.com但也能匹配anyprefixexample.com因为.*会吃掉前缀和点号一不小心就把无关域名囊括进来了。排查这类问题时先确认你到底要域名的哪一部分固定固定部分写成字面量可变部分才用通配符或正则。3.4 默认 server 块位置导致的灵异响应前面说过匹配不到 Host 时选第一个 server 块。如果你的配置文件里第一个 server 块是某个重要站点的完整配置那所有无法识别的请求都会返回那个站点的页面流量统计、日志分析全乱套。我见过一个案例某公司服务器同时服务十几个客户的站点其中一个客户投诉说访问别的域名时偶尔看到他们的页面。查下来是另一个客户的配置里 server_name 写漏了一个域名那个域名恰好没匹配上任何块于是落到了第一个块——也就是投诉客户的那个站点。这不是 Nginx 的 bug纯粹是兜底机制在起作用。保险的做法是在每个 listen 端口上显式定义一个 default_server 块建议统一配置成返回 404 或 444避免任何意外访客拿到真实站点内容。3.5 HTTPS 和 SNIserver_name 在多证书场景下的配合HTTPS 场景中server_name 的作用还有一个前置步骤TLS 握手时需要根据 SNIServer Name Indication选择证书。Nginx 会先根据 SNI 选证书再根据 Host 头选 server 块。如果这两个信息不匹配比如用户访问a.example.com但 SNI 携带的是b.example.com就可能出现证书对不上号的情况。多证书场景的配置原则是每个域名各自的 server 块要独立 listen 443并且带上对应的证书server { listen 443 ssl; server_name a.example.com; ssl_certificate /etc/nginx/certs/a.crt; ssl_certificate_key /etc/nginx/certs/a.key; } server { listen 443 ssl; server_name b.example.com; ssl_certificate /etc/nginx/certs/b.crt; ssl_certificate_key /etc/nginx/certs/b.key; }另外注意如果两个 server 块 server_name 都包含a.example.com且 listen 相同Nginx 会挑第一个块处理而证书可能因此选错。配置多证书前一定要检查 server_name 是否交叉重叠。4. 多域名、泛解析与默认站点一套可以照抄的配置模板讲完坑给出一套我实际生产环境里用过的配置骨架覆盖主域 泛子域、多域名合并、默认站点兜底、HTTP 跳 HTTPS 这四类常见需求。4.1 主域名 泛子域名解析到同一套服务需求example.com和任意子域名*.example.com都指向同一个应用。server { listen 80; listen 443 ssl; http2 on; server_name example.com *.example.com; ssl_certificate /etc/nginx/certs/example.pem; ssl_certificate_key /etc/nginx/certs/example.key; root /var/www/example; index index.html; location / { try_files $uri $uri/ /index.html; } }关键点在于server_name example.com *.example.com;这行裸域和泛子域一起声明。如果只写*.example.com主域名访问会被漏掉落入默认 server。4.2 多域名合并到同一站点但区分来源如果两个域名指向同一个应用但你想在日志里区分来源或者在响应头里标识不同入口可以这样server { listen 80; server_name domain-a.com www.domain-a.com domain-b.com www.domain-b.com; access_log /var/log/nginx/access.log combined; set $entry_domain $host; add_header X-Entry-Domain $entry_domain always; }这里用$host变量直接记录原始 Host不需要为每个域名单写一个 server 块。必要的时候可以在location里用if ($host ...)做分支处理但小心if的滥用能用变量就别用if。4.3 默认站点兜底的完整写法兜底块建议放在配置文件最前面或单独文件里明确负责所有漏网之鱼。server { listen 80 default_server; listen 443 ssl default_server; server_name _; ssl_certificate /etc/nginx/certs/default.pem; ssl_certificate_key /etc/nginx/certs/default.key; return 444; }return 444是 Nginx 特有的响应码它不会发任何响应给客户端而是直接断开连接。对端口扫描、恶意探测这类请求非常友好省流量、省日志噪音。4.4 HTTP 强制跳转 HTTPS 的推荐配置统一在 80 端口的默认块做跳转比在每个站点块里重复写跳转逻辑更省事server { listen 80 default_server; server_name _; return 301 https://$host$request_uri; }但要注意如果某些老客户端只支持 HTTP或者你的某些 API 必须走 HTTP这样的全局跳转会误伤。实际场景中我更喜欢在具体域名的 80 端口块里做精确跳转只在确信所有流量都要 HTTPS 时才用上面的写法。5. 排查与验证技巧nginx -T curl 实测匹配结果配置写完不是终点验证才是。排查 server_name 相关问题时我一般按下面三个步骤来。5.1 先看生效配置nginx -Tnginx -T大写 T会输出解析后的完整配置按文件包含顺序展开。这一步能确认你编辑的配置是否真的被加载、有没有语法被覆盖。比如有的server_name 被后面的同名指令覆盖了肉眼很难发现但nginx -T里看得一清二楚。nginx -t # 只检查语法 nginx -T # 输出全部有效配置很多 server_name 相关的诡异问题第一步就能在这里找到线索确认值没有被变量替换、没有被 include 的顺序覆盖。5.2 手动指定 Host 实测用 curl 手动指定 Host 头可以绕过 DNS 直接测试服务器的匹配行为curl -H Host: www.example.com http://127.0.0.1/ curl -H Host: api.example.com http://127.0.0.1/ curl -H Host: unknown-domain.com http://127.0.0.1/把三个请求的响应头和响应体对比一下基本能判断出每个 Host 都落到了哪个 server 块。如果结果不符合预期再配合配置文件确认 server_name 写法和默认块位置。这个技巧尤其适合在切换 DNS 之前做预验证。因为域名解析没切过来时线上流量不会走到新服务器但你完全可以本地指定 Host 来测试新服务器的匹配结果。5.3 开启 debug 日志查看选择过程如果 curl 验证还不够直观可以临时开一下 debug 日志error_log /var/log/nginx/error.log debug;然后刷新请求在日志里搜索server name或server关键字能看到 Nginx 内部对每个候选 server 块的评分过程哪些候选被否决了原因是什么。这个输出一般只在排查复杂匹配问题时需要平时建议保持默认级别debug 日志量大、消耗性能。5.4 一个小的自检清单最后分享一个自检清单每次写完 server_name 配置都按这个过一遍裸域和泛子域是否同时覆盖*.example.com不会匹配example.com有没有显式设置 default_server兜底块返回什么正则中的点号是否转义有没有用^和$锚定HTTPS 场景下443 端口是否重复声明了同一个 server_name多个证书域名是否彼此交叉重叠这套检查我用了很久基本能拦下九成以上的 server_name 坑。配置这种东西出事往往不在语法而在语义——你写的时候觉得应该没问题和机器实际匹配的结果是两回事。多花三分钟跑一遍验证省下的可能是半夜爬起来改配置的代价。
上一篇/下一篇内容由系统自动关联
返回资讯列表 →