背景
在早前 初识Nginx中server和location的匹配顺序 中有简单提到 server_name
的匹配顺序。然而,在日常的使用过程中不免会遇到一些”坑”,在不断踩坑的过程中加深了对现有知识的理解,并借此机会进行了补充。
关于 server_name 的匹配顺序
以下内容其实在 How nginx processes a request 也有说明,下文主要是基于日常配置过程中加以补充。
- listen 匹配(作为大前提,如果访问端口都不对,就不会进行后续的域名匹配)
- 精确域名匹配
- 以
*
开头,最长匹配的泛域名匹配,如:*.test.com
- 以
*
结尾,最长匹配的泛域名匹配,如:www.test.*
- 正则域名匹配,如:
~^\.www\.test\.com$
- 如果都不匹配
- 优先选择 listen 配置项后有 default 或 default_server 的 server 块匹配
- 根据
server_name localhost
匹配 (当且仅当请求 Host 为 localhost 且server_name
也为 localhost 时才生效) - 根据 Nginx 配置文件/目录中,
ls -l
结果从上至下(按 0-9 -> a-z)顺序匹配,匹配到的第一个符合条件的即止- 例如: 有 80 监听的第一个 server 块,忽略
server_name
,直接匹配对应 location
- 例如: 有 80 监听的第一个 server 块,忽略
注意事项
- 在条件允许的情况下,请尽量使用精确域名;
- 当出现多个相同的
server_name
时,会根据站点配置文件排序优先匹配; - 在普通
server_name
配置下,*
符号可以代表多字符匹配,但只能用在server_name
的开头或末尾;- 例如:
www.*.example.org
和w*.example.org
是无效的
- 例如:
- Nginx 的正则表达式与 Perl 语言(PCRE)使用的正则表达式兼容;
- 正则匹配必须以
~
开头(否则将会认为是精确字符匹配),并建议使用^
和$
符号分别作为”开头”和”结束”的锚点;- 例如:
server_name ~^www\d+\.example\.net$;
- 例如:
- 正则域名下允许在
server_name
中使用*
符号匹配多字符;- 例如:
~^w.*\.example\.org$
不仅匹配www.example.org
,还匹配www.sub.example.org
- 例如:
关于泛域名匹配
泛域名配置
根据 Server names#wildcard_names 中说明,在普通泛域名配置中,*
符号只能对域名”前缀”或”后缀”进行配置,其他位置则不生效。
server {
listen 80;
server_name *.example.org;
...
}
server {
listen 80;
server_name mail.*;
...
}
正则泛域名配置
如果想对固定域名的部分字符进行泛域名匹配时(例如匹配: api.dev.nestealin.com
、api.pre.nestealin.com
、api.prod.nestealin.com
等 api.*.nestealin.com
格式的域名),则可以使用正则表达式的方式进行。
server {
listen 80;
server_name ~^api\.*\.nestealin\.com$;
...
}
泛域名误区 - “无意义域名”
在一些教程网站中,也有一些配置会用 server_name ""
或 server_name _
来表示”泛域名”匹配,但这其实并不完全符合匹配原则,只是”恰好”在规则条件上得以命中,看似解决了这方面问题罢了。下面将针对最常见的几个”泛域名” server_name
配置进行主要说明。
server_name “”
Nginx 的官方文档是这么描述 server_name ""
的:
If it is required to process requests without the “Host” header field in a server block which is not the default, an empty name should be specified.
也就是说,如果想让一个没有被标注为 default_server
的 server 块来处理一个没有带有 Host header 的请求的话,就需要使用 server_name ""
指令。但问题是,在 RFC 7230, Section 5.4 中 HTTP/1.1 (也就是当前主流的 HTTP version)对 Host 的规定[^3],如果一个请求没有带有 Host header 的话,服务器必须得返回 400(Bad Request):
所以,使用了 server_name ""
的非 default server 块压根就没有机会去处理一个没有带有 Host header 的请求,因为在把这个请求交给 server 块之前就已经先被 Nginx 处理然后响应 400 状态码了。(即便客户端为 HTTP/1.0 结果也是同样)
因此,当 server_name ""
单独使用时,该 server 块是处理不了任何请求的(按正常匹配顺序来说),它通常要跟 listen 中的 default
或 default_server
配合使用,表示这个 server 块可以集中处理那些无法被其他 server 块处理的请求。
server_name _ | server_name $ | server_name @
server_name _
是可以匹配到 Host header 的值的,因为它可以匹配到 Host header 值为 “_
“ 的请求。但是这种匹配其实也是没有意义的,因为对于浏览器而言,请求中的 Host header 的值就是你所输入的网站地址(可以是 IP 地址也可以是域名),但是 “_
“ 根本就不是一个域名,它根本就无法被解析成 ip 地址,无法被解析成 ip 地址就意味着这个请求根本就没办法发送到对应的服务器上,所以 server_name _
跟 server_name ""
一样根本就没有用武之地。
同样地,我们可以认为 server_name _
跟 server_name ""
一样本身是没有任何意义;其实除了 server_name _
之外,像 server_name !
、 server_name $
、 server_name @
、server_name --
等这类都是属于”无意义域名”。
因此,server_name _
其实跟 server_name ""
一样,在 server_name _
单独使用的 server 块中是处理不了任何请求的,一般也是要跟 listen 中的 default
或 default_server
一起配合使用才有最佳效果,也是表示这个 server 块可以集中处理那些无法被其他 server 块处理的请求。
关于国际化域名
什么是国际化域名
在全球域名规范中,除了英文点分域名格式以外,还可能存在中文、俄文等国家文字作为域名,统称为国际化域名( Internationalized Domain Names (IDNs) )[^1][^2]。
如何配置国际化域名
假设现在要为一个俄文域名(пример.испытание
)进行配置,此时就需要将域名转为 ASCII (Punycode) 格式。
例如:
server {
listen 80;
server_name xn--e1afmkfd.xn--80akhbyknj4f; # 实际域名: пример.испытание
...
}
Punycode 转换
在线转换工具: Punycode编码转换
关于匹配规则的实验
环境说明
服务端(Nginx)
Nginx IP: 192.168.7.27
在 Nginx 配置目录(/usr/local/nginx/conf/sites
)下创建两个配置文件:
注: 为了避免”兜底配置”在站点文件的首位,因此,在本次实验中会将”兜底配置”文件根据匹配规则放到目录”末尾”。
正常域名配置: (a-test.conf)
server {
listen 80;
server_name abc.nestealin.com;
location / {
add_header content-type application/json;
return 200 '{"status": "This is the a-test.conf"}';
}
}
兜底域名配置: (z-default_test.conf)
server {
#listen 80 default_server ;
listen 80 ;
#server_name "";
server_name _;
#server_name $;
#server_name localhost;
#server_name ~^(.+)$;
#server_name ~^.*$;
#server_name ~. "";
location / {
add_header content-type application/json;
return 200 '{"status": "This is the z-default_test.conf"}';
}
}
客户端
使用 curl 命令进行请求测试:
- curl 版本: 8.4.0
- 请求协议版本: HTTP/1.1
验证方式
在”正常域名”配置不变的情况下,不断修改兜底域名配置,客户端重复请求进行验证。
实验过程
server_name “” 并且是 default_server
客户端请求携带 Host
# curl http://192.168.7.27 -v
* Trying 192.168.7.27:80...
* Connected to 192.168.7.27 (192.168.7.27) port 80
> GET / HTTP/1.1
> Host: 192.168.7.27
> User-Agent: curl/8.4.0
> Accept: */*
>
< HTTP/1.1 200 OK
< Server: nginx
< Date: Fri, 05 Apr 2024 07:47:51 GMT
< Content-Type: application/octet-stream
< Content-Length: 37
< Connection: close
< content-type: application/json
<
* Closing connection
{"status": "This is the a-test.conf"}
客户端请求不携带 Host
# curl 192.168.7.27 -v -H "Host: "
* Trying 192.168.7.27:80...
* Connected to 192.168.7.27 (192.168.7.27) port 80
> GET / HTTP/1.1
> Host:
> User-Agent: curl/8.4.0
> Accept: */*
>
< HTTP/1.1 400 Bad Request
< Server: nginx
< Date: Fri, 05 Apr 2024 03:51:50 GMT
< Content-Type: text/html; charset=utf-8
< Content-Length: 150
< Connection: close
<
<html>
<head><title>400 Bad Request</title></head>
<body>
<center><h1>400 Bad Request</h1></center>
<hr><center>nginx</center>
</body>
</html>
* Closing connection
server_name “” 但不是 default_server
Nginx 兜底域名配置改为如下
server {
listen 80;
server_name "";
location / {
add_header content-type application/json;
return 200 '{"status": "ok"}';
}
}
客户端请求携带 Host
# curl 192.168.7.27 -v
* Trying 192.168.7.27:80...
* Connected to 192.168.7.27 (192.168.7.27) port 80
> GET / HTTP/1.1
> Host: 192.168.7.27
> User-Agent: curl/8.4.0
> Accept: */*
>
< HTTP/1.1 200 OK
< Server: nginx
< Date: Fri, 05 Apr 2024 08:16:27 GMT
< Content-Type: application/octet-stream
< Content-Length: 45
< Connection: close
< content-type: application/json
<
* Closing connection
{"status": "This is the z-default_test.conf"}
客户端请求不携带 Host
# curl http://192.168.7.27 -v -H "Host: "
* Trying 192.168.7.27:80...
* Connected to 192.168.7.27 (192.168.7.27) port 80
> GET / HTTP/1.1
> Host:
> User-Agent: curl/8.4.0
> Accept: */*
>
< HTTP/1.1 400 Bad Request
< Server: nginx
< Date: Fri, 05 Apr 2024 03:49:14 GMT
< Content-Type: text/html; charset=utf-8
< Content-Length: 150
< Connection: close
<
<html>
<head><title>400 Bad Request</title></head>
<body>
<center><h1>400 Bad Request</h1></center>
<hr><center>nginx</center>
</body>
</html>
* Closing connection
实验结果
由于繁复性结果相近,所以省略了其他过程内容,以下是整体结果:
当兜底配置的 listen 仅为 80 时
即类似如下配置:
server{
listen 80;
server_name XXXX;
...
}
注:
- 表格首列为客户端的请求 Host;
- 表格首行为 Nginx 站点配置中的
server_name
配置;- 表格内容为 Nginx 的响应状态码/请求命中的站点配置;
访问Host | server_name "" |
server_name _ |
server_name localhost |
server_name ~^.*$ |
---|---|---|---|---|
Null(空) | 400 | 400 | 400 | 400 |
localhost | a-test.conf | a-test.conf | z-default_test.conf | z-default_test.conf |
abc.nestealin.com | a-test.conf | a-test.conf | a-test.conf | a-test.conf |
a1b2c3.nestealin.com | a-test.conf | a-test.conf | a-test.conf | z-default_test.conf |
192.168.1.1 | a-test.conf | a-test.conf | a-test.conf | z-default_test.conf |
192.168.7.27 | a-test.conf | a-test.conf | a-test.conf | z-default_test.conf |
127.0.0.1 | a-test.conf | a-test.conf | a-test.conf | z-default_test.conf |
当兜底配置的 listen 为 80 default_server 时
即类似如下配置:
server{
listen 80 default_server;
server_name XXXX;
...
}
注:
- 表格首列为客户端的请求 Host;
- 表格首行为 Nginx 站点配置中的
server_name
配置;- 表格内容为 Nginx 的响应状态码/请求命中的站点配置;
访问Host | server_name "" |
server_name _ |
server_name localhost |
server_name ~^.*$ |
---|---|---|---|---|
Null(空) | 400 | 400 | 400 | 400 |
localhost | z-default_test.conf | z-default_test.conf | z-default_test.conf | z-default_test.conf |
abc.nestealin.com | a-test.conf | a-test.conf | a-test.conf | a-test.conf |
a1b2c3.nestealin.com | z-default_test.conf | z-default_test.conf | z-default_test.conf | z-default_test.conf |
192.168.1.1 | z-default_test.conf | z-default_test.conf | z-default_test.conf | z-default_test.conf |
192.168.7.27 | z-default_test.conf | z-default_test.conf | z-default_test.conf | z-default_test.conf |
127.0.0.1 | z-default_test.conf | z-default_test.conf | z-default_test.conf | z-default_test.conf |
实验小结
- 无论是否存在默认监听(
listen ${Port} default_server;
),当请求 Host 为空时,响应结果均为 400; - 当没有默认监听且请求 Host 为 localhost 时,如果存在
server_name localhost
的话,在 Nginx 看来该server_name
也是一个”精确域名”,可以命中对应 server 配置; - 当没有默认监听且请求 Host 不为空时,
server_name
匹配顺序: 精准域名 > 泛域名 > 正则域名 >localhost
(如果恰好有 Host 也为 localhost 的话) > 站点(配置文件)顺序;即”无意义域名”(server_name _
等)并非真正意义上的”兜底域名”; - 当存在默认监听且请求 Host 不为空时,”无意义域名”、”localhost”、”
~^.*$
“配置等价,都能作为”兜底域名”存在,实现拦截对应 listen 端口中所有 server 配置外的 HTTP 请求;
总结
- 在条件允许的情况下,应尽量使用精确域名,其次泛域名,最后是正则域名,请尽量少用无意义域名;
- 在使用正则域名时,必须以
~
开头(否则将会认为是精确字符匹配),并建议使用^
和$
符号分别作为”开头”和”结束”的锚点; - 在 listen 指令中,
default
与default_server
作用相同,在 0.8.21 及以后的 Nginx 版本中,推荐使用default_server
更符合官方规范; - 当请求 Host 为空时,Nginx 会遵循 RFC 7230, Section 5.4 中的规定[^3]直接响应 400 状态码,无视所有匹配规则;
- 当
server_name ""
等无意义域名期望作为”兜底配置”存在时,其 listen 指令必须要有default_server
参数,否则可能会因匹配顺序而无法实现相应兜底逻辑; - 当没有默认监听且请求 Host 为 localhost 时,如果存在
server_name localhost
的配置,则该server_name
也是一个”精确域名”,可以命中对应 server 配置;
最佳实践
1. 匹配 80 端口的所有 HTTP 请求 (兜底)
写法一: 正则域名
server {
listen 80 default_server;
server_name ~^.*$;
access_log logs/default.access.log main;
...
}
通常,这种配置会作为兜底模块,其 listen 也建议设置成 default_server
进行兜底拦截,实现捕获所有规则外的请求。
注: 在 Nginx 0.8.21 之前的版本里,需要”默认监听”使用”
default
“进行声明;从 0.8.21 开始的版本新加入了”default_server
“参数,其作用与”default
“等价并兼容旧版本写法,可以选择任一声明。
写法二: 无意义域名
server {
listen 80 default_server;
server_name _;
access_log logs/default.access.log main;
...
}
虽然不推荐这种方式,但其匹配顺序可以作为正则域名后的补充,可以提供参考。(例如: 存在大量正则域名,怕影响现存正则写法的情况。)
注意: 该配置的
default_server
不能去除,否则可能导致配置无效。
2. 匹配 80 端口所有 HTTP 请求并响应断开连接 (兜底)
如果 Nginx 对客户端请求有要求,客户端必须携带合规的 “Host” 标头字段才能访问(即自己没有配置的 server_name
都不允许访问),则可以定义一个 server 块来直接丢弃非法请求:
注: “Host”字段可以是 IP 地址,也可以是域名。
写法一: 正则域名
server {
listen 80 default_server;
server_name ~^.*$;
access_log logs/default.access.log main;
return 444;
}
写法二: 无意义域名
server {
listen 80 default_server;
server_name _;
access_log logs/default.access.log main;
return 444;
}
References
- Nginx Doc - Server names
- 我所理解的 nginx 中 server_name “” 与 server_name _ 之间的区别 - Nginx - NICECHI 博客
- virtualhost - What is the difference between server_name _ and server_name “” in Nginx? - Server Fault
- How nginx processes a request
- Server Block Examples | NGINX
- nginx中server_name查询顺序 — Learning Notebook v1.0 documentation
- Nginx常见问题 - gong^_^ - 博客园
[^1]: Internationalized Domain Name - ICANNWIki
[^2]: Internationalized domain name - Wikipedia
[^3]: RFC 7230 - Hypertext Transfer Protocol (HTTP/1.1): Message Syntax and Routing