一文理清nginx中的location配置

location 指令是 nginx 中最关键的指令之一,location 指令的功能是用来匹配不同的 URI 请求,进而对请求做不同的处理和响应,这其中较难理解的是多个 location 的匹配顺序,本文会作为重点来解释和说明。

开始之前先明确一些约定,我们输入的网址叫做请求 URI,nginx 用请求 URI 与 location 中配置的 URI 做匹配。

nginx文件结构

首先我们先简单了解 nginx 的文件结构,nginx 的 HTTP 配置主要包括三个区块,结构如下:

1
2
3
4
5
6
7
Global: nginx 运行相关
Events: 与用户的网络连接相关
http
http Global: 代理,缓存,日志,以及第三方模块的配置
server
server Global: 虚拟主机相关
location: 地址定向,数据缓存,应答控制,以及第三方模块的配置

从上面展示的 nginx 结构中可以看出 location 属于请求级别配置,这也是我们最常用的配置。

配置 location 块

location 语法

Location 块通过指定模式来与客户端请求的URI相匹配。
Location基本语法:

  • 匹配 URI 类型,有四种参数可选,当然也可以不带参数。
  • 命名location,用@来标识,类似于定义goto语句块。
1
2
location [ = | ~ | ~* | ^~ ] /URI { … }
location @/name/ { … }

location匹配命令解释

参数 解释
location 后没有参数直接跟着 标准 URI,表示前缀匹配,代表跟请求中的 URI 从头开始匹配。
= 用于标准 URI 前,要求请求字符串与其精准匹配,成功则立即处理,nginx停止搜索其他匹配。
^~ 用于标准 URI 前,并要求一旦匹配到就会立即处理,不再去匹配其他的那些个正则 URI,一般用来匹配目录
~ 用于正则 URI 前,表示 URI 包含正则表达式, 区分大小写
~\* 用于正则 URI 前, 表示 URI 包含正则表达式, 不区分大小写
@ @ 定义一个命名的 location,@ 定义的locaiton名字一般用在内部定向,例如error_page, try_files命令中。它的功能类似于编程中的goto。

location匹配顺序

nginx有两层指令来匹配请求 URI 。第一个层次是 server 指令,它通过域名、ip 和端口来做第一层级匹配,当找到匹配的 server 后就进入此 server 的 location 匹配。

location 的匹配并不完全按照其在配置文件中出现的顺序来匹配,请求URI 会按如下规则进行匹配:

  1. 先精准匹配 = ,精准匹配成功则会立即停止其他类型匹配;
  2. 没有精准匹配成功时,进行前缀匹配。先查找带有 ^~ 的前缀匹配,带有 ^~ 的前缀匹配成功则立即停止其他类型匹配,普通前缀匹配(不带参数 ^~ )成功则会暂存,继续查找正则匹配;
  3. =^~ 均未匹配成功前提下,查找正则匹配 ~~\* 。当同时有多个正则匹配时,按其在配置文件中出现的先后顺序优先匹配,命中则立即停止其他类型匹配;
  4. 所有正则匹配均未成功时,返回步骤 2 中暂存的普通前缀匹配(不带参数 ^~ )结果

以上规则简单总结就是优先级从高到低依次为(序号越小优先级越高):

1
2
3
4
5
6
1. location =    # 精准匹配
2. location ^~ # 带参前缀匹配
3. location ~ # 正则匹配(区分大小写)
4. location ~* # 正则匹配(不区分大小写)
5. location /a # 普通前缀匹配,优先级低于带参数前缀匹配。
6. location / # 任何没有匹配成功的,都会匹配这里处理

上述匹配规则可以用以下伪代码表示,加深理解:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function match(uri):
rv = NULL

if uri in exact_match:
return exact_match[uri]

if uri in prefix_match:
if prefix_match[uri] is '^~':
return prefix_match[uri]
else:
rv = prefix_match[uri] // 注意这里没有 return,且这里是最长匹配

if uri in regex_match:
return regex_match[uri] // 按文件中顺序,找到即返回
return rv

案例分析

接下来,让我们通过一些实际案例来验证上述规则。

案例 1
1
2
3
4
5
6
7
8
9
10
11
server {
server_name website.com;
location /doc {
return 701; # 用这样的方式,可以方便的知道请求到了哪里
}
location ~* ^/document$ {
return 702;

}
}
curl -I website.com:8080/document` 返回 `返回 HTTP/1.1 702

说明:按照上述的规则,显然第二个正则匹配会有更高的优先级

案例 2
1
2
3
4
5
6
7
8
9
10
server {
server_name website.com;
location /document {
return 701;
}
location ~* ^/document$ {
return 702;
}
}
curl -I website.com:8080/document` 返回 `HTTP/1.1 702

说明:第二个匹配了正则表达式,优先级高于第一个普通前缀匹配

案例 3
1
2
3
4
5
6
7
8
9
10
server {
server_name website.com;
location ^~ /doc {
return 701;
}
location ~* ^/document$ {
return 702;
}
}
curl http://website.com/document` 返回 `HTTP/1.1 701

说明:第一个前缀匹配 ^~ 命中以后不会再搜寻正则匹配,所以会第一个命中。

案例 4
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
server {
server_name website.com;
location /docu {
return 701;
}
location /doc {
return 702;
}
}
curl -I website.com:8080/document` 会返回 `HTTP/1.1 701
server {
server_name website.com;
location /doc {
return 702;
}
location /docu {
return 701;
}
}
curl -I website.com:8080/document` 依然返回 `HTTP/1.1 701

说明:前缀匹配下,返回最长匹配的 location,与 location 所在位置顺序无关

案例 5
1
2
3
4
5
6
7
8
9
10
11
12
13
server {
listen 8080;
server_name website.com;

location ~ ^/doc[a-z]+ {
return 701;
}

location ~ ^/docu[a-z]+ {
return 702;
}
}
curl -I website.com:8080/document` 返回 `HTTP/1.1 701

把顺序换一下

1
2
3
4
5
6
7
8
9
10
11
12
13
server {
listen 8080;
server_name website.com;

location ~ ^/docu[a-z]+ {
return 702;
}

location ~ ^/doc[a-z]+ {
return 701;
}
}
curl -I website.com:8080/document` 返回 `HTTP/1.1 702

说明:可见正则匹配是使用文件中的顺序,先匹配成功的返回。

案例 6

我们对一个官方文档中提到例子做一些补充,来看一个相对较完整的例子,假设我们有如下几个请求等待匹配:

1
2
3
4
5
6
/
/index.html
/documents/document.html
/documents/abc
/images/a.gif
/documents/a.jpg

以下是 location 配置及其匹配情况

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
location  = / {
# 只精准匹配 / 的查询.
[ configuration A ]
}
# 匹配成功: /

location / {
# 匹配任何请求,因为所有请求都是以”/“开始
# 但是更长字符匹配或者正则表达式匹配会优先匹配
[ configuration B ]
}
#匹配成功:/index.html

location /documents {
# 匹配任何以 /documents/ 开头的地址,匹配符合以后,还要继续往下搜索/
# 只有后面的正则表达式没有匹配到时,这一条才会采用这一条/
[ configuration C ]
}
# 匹配成功:/documents/document.html
# 匹配成功:/documents/abc

location ~ /documents/ABC {
# 区分大小写的正则匹配
# 匹配任何以 /documents/ 开头的地址,匹配符合以后,还要继续往下搜索/
# 只有后面的正则表达式没有匹配到时,这一条才会采用这一条/
[ configuration CC ]
}

location ^~ /images/ {
# 匹配任何以 /images/ 开头的地址,匹配符合以后,立即停止往下搜索正则,采用这一条。/
[ configuration D ]
}
# 成功匹配:/images/a.gif

location ~* \.(gif|jpg|jpeg)$ {
# 匹配所有以 .gif、.jpg 或 .jpeg 结尾的请求,不区分大小写
# 然而,所有请求 /images/ 下的图片会被 [ config D ] 处理,因为 ^~ 到达不了这一条正则/
[ configuration E ]
}
# 成功匹配:/documents/a.jpg

location /images/ {
# 字符匹配到 /images/,继续往下,会发现 ^~ 存在/
[ configuration F ]
}

location /images/abc {
# 最长字符匹配到 /images/abc,继续往下,会发现 ^~ 存在/
# F与G的放置顺序是没有关系的/
[ configuration G ]
}

location ~ /images/abc/ {
# 只有去掉 [ config D ] 才有效:先最长匹配 [ config G ] 开头的地址,继续往下搜索,匹配到这一条正则,采用/
[ configuration H ]
}

其他location配置相关

匹配问号后的参数

请求 URI 中问号后面的参数是不能在 location 中匹配到的,这些参数存储在 $query_string 变量中,可以用 if 来判断。
例如,对于参数中带有单引号 进行匹配然后重定向到错误页面。

1
2
3
4
/plus/list.php?tid=19&mid=1124‘
if ( $query_string ~* “.*[;’<>].*” ){
return 404;
}

location URI结尾带不带 /

关于 URI 尾部的 / 有三点也需要说明一下。第一点与 location 配置有关,其他两点无关。

  1. location 中的字符有没有 / 都没有影响。也就是说 /user//user 是一样的。
  2. 如果 URI 结构是 https://domain.com/ 的形式,尾部有没有 / 都不会造成重定向。因为浏览器在发起请求的时候,默认加上了 / 。虽然很多浏览器在地址栏里也不会显示 / 。这一点,可以访问baidu验证一下。
  3. 如果 URI 的结构是 https://domain.com/some-dir/ 。尾部如果缺少 / 将导致重定向。因为根据约定,URL 尾部的 / 表示目录,没有 / 表示文件。所以访问 /some-dir/ 时,服务器会自动去该目录下找对应的默认文件。如果访问 /some-dir 的话,服务器会先去找 some-dir 文件,找不到的话会将 some-dir 当成目录,重定向到 /some-dir/ ,去该目录下找默认文件。可以去测试一下你的网站是不是这样的。

命名 location

带有 @ 的 location 是用来定义一个命名的 location,这种 location 不参与请求匹配,一般用在内部定向。用法如下:

1
2
3
4
5
6
location / {
try_files $uri $uri/ @custom
}
location @custom {
# ...do something
}

上例中,当尝试访问 URI 找不到对应的文件就重定向到我们自定义的命名 location(此处为 custom)。

值得注意的是,命名 location 中不能再嵌套其它的命名 location。

location 实际使用建议

所以实际使用中,个人觉得至少有三个匹配规则定义,如下:

直接匹配网站根,通过域名访问网站首页比较频繁,使用这个会加速处理,官网如是说。

这里是直接转发给后端应用服务器了,也可以是一个静态首页。第一个必选规则:

1
2
3
location = / {
proxy_pass http://tomcat:8080/index
}

第二个必选规则是处理静态文件请求,这是 nginx 作为 http 服务器的强项,有两种配置模式,目录匹配或后缀匹配,任选其一或搭配使用:

1
2
3
4
5
6
location ^~ /static/ {
root /webroot/static/;
}
location ~* \.(gif|jpg|jpeg|png|css|js|ico)$ {
root /webroot/res/;
}

第三个规则就是通用规则,用来转发动态请求到后端应用服务器,非静态文件请求就默认是动态请求,自己根据实际把握,毕竟目前的一些框架的流行,带.php,.jsp后缀的情况很少了:

1
2
3
location / {
proxy_pass http://tomcat:8080/
}

nginx 通过 ngx_http_rewrite_module 模块支持 URI 重写、支持 if 条件判断,但不支持 else。

rewrite 只能放在 server { }location { }if { } 中,并且只能对域名后边的除去传递的参数外的字符串起作用,例如http://aaa.com/a/we/index.php?id=1&u=str只对/a/we/index.php重写。语法为 rewrite regex replacement [flag];

指令执行顺序

表面看 rewrite 和 location 功能有点像,都能实现跳转,主要区别在于 rewrite 是在同一域名内更改获取资源的路径,而 location 是对一类路径做控制访问或反向代理,可以 proxy_pass 到其他机器。很多情况下 rewrite 也会写在 location 里,它们的执行顺序是:

  1. 执行 server 块的 rewrite 指令(这里的块指的是 server 关键字后{}包围的区域,其它 xx 块类似)
  2. 执行location匹配
  3. 执行选定的location中的rewrite指令

如果其中某步 URI 被重写,则重新循环执行1-3,直到找到真实存在的文件;
如果循环超过 10 次,则返回 500 Internal Server Error 错误。

指令详解

if 指令

语法:if(condition) {…}
作用域: serverlocation
功能:对给定的条件 condition 进行判断。如果为真,大括号内的 rewrite 指令将被执行。if 条件 (conditon) 可以是如下任何内容:
if 中的几种判断条件

  • 一个变量名;false 如果这个变量是空字符串或者以0开始的字符串;
  • 使用 =!= 比较的一个变量和字符串
  • 是用 ~~* 与正则表达式匹配的变量,如果这个正则表达式中包含},;则整个表达式需要用” 或’ 包围
  • 使用 -f!-f 检查一个文件是否存在
  • 使用 -d!-d 检查一个目录是否存在
  • 使用 -e!-e 检查一个文件、目录、符号链接是否存在
  • 使用 -x !-x 检查一个文件是否可执行

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
set $variable "0"; 
if ($variable) {
# 不会执行,因为 "0" 为 false
break;
}

# 使用变量与正则表达式匹配 没有问题
if ( $http_host ~ "^star\.igrow\.cn$" ) {
break;
}

# 字符串与正则表达式匹配 报错
if ( "star" ~ "^star\.igrow\.cn$" ) {
break;
}
# 检查文件类的 字符串与变量均可
if ( !-f "/data.log" ) {
break;
}

if ( !-f $filename ) {
break;
}

return 指令

语法:
return code [text];
return code URL;
return URL;
作用域:serverlocationif
功能:停止处理并将指定的 code 码返回给客户端。 非标准 code 码 444 关闭连接而不发送响应报头。

该指令用于检查一个条件是否符合,如果条件符合,则执行大括号内的语句。If 指令不支持嵌套,不支持多个条件 && 和 || 处理。

从0.8.42版本开始, return 语句可以指定重定向 URI (状态码可以为如下几种 301,302,303,307),

也可以为其他状态码指定响应的文本内容,并且重定向的 URI 和响应的文本可以包含变量。

有一种特殊情况,就是重定向的url可以指定为此服务器本地的 URI,这样的话,nginx 会依据请求的协议 $schemeserver_name_in_redirectport_in_redirect 自动生成完整的 URI。

示例:如果访问的 URI 以 .sh.bash 结尾,则返回 403 状态码

1
2
3
4
location ~ .*\.(sh|bash)?$
{
return 403;
}

rewrite 指令

语法:rewrite regex replacement [flag];
作用域:serverlocationif
功能:如果一个URI匹配指定的正则表达式regex,URI就按照 replacement 重写。

rewrite 按配置文件中出现的顺序执行。可以使用 flag 标志来终止指令的进一步处理。

如果 replacement 以 http://https://$ scheme 开始,将不再继续处理,这个重定向将返回给客户端。

示例:第一种情况,重写的字符串带 http://

1
2
3
4
5
6
location ^~ /redirect {
# 当匹配前缀表达式 /redirect/(.*)时 请求将被临时重定向到 http://www.$1.com
# 相当于 flag 写为 redirect
rewrite ^/(.*)$ http://www.$1.com;
return 200 "ok";
}

在浏览器中输入 127.0.0.1:8080/redirect/baidu ,则临时重定向到 www.baidu.com 后面的 return 指令将没有机会执行了。

1
2
3
4
5
6
7
location ^~ /redirect {
rewrite ^/(.*)$ www.$1.com;
return 200 "ok";
}
# 发送请求如下
# curl 127.0.0.1:8080/redirect/baidu
# ok

此处没有带 http:// 所以只是简单的重写。请求的 URI 由 /test1/baidu 重写为 www.baidu.com 因为会顺序执行 rewrite 指令,所以 下一步执行 return 指令,响应后返回 ok

flag 有四种参数可以选择:

  1. last 停止处理后续 rewrite 指令集,然后对当前重写的新 URI 在 rewrite 指令集上重新查找。
  2. break 停止处理后续 rewrite 指令集,并不再重新查找,但是当前location 内剩余非 rewrite 语句和 location 外的 非rewrite 语句可以执行。
  3. redirect 如果 replacement 不是以 http://https:// 开始,返回 302 临时重定向
  4. permanent 返回 301 永久重定向

示例 1:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# rewrite 后面没有任何 flag 时就顺序执行 
# 当 location 中没有 rewrite 模块指令可被执行时 就重写发起新一轮location 匹配
location / {
# 顺序执行如下两条rewrite指令
rewrite ^/test1 /test2;
rewrite ^/test2 /test3; # 此处发起新一轮 location 匹配 URI为/test3
}

location = /test2 {
return 200 “/test2”;
}

location = /test3 {
return 200 “/test3”;
}
# 发送如下请求
# curl 127.0.0.1:8080/test1
# /test3

如果正则表达regex式中包含 “}” 或 “;”,那么整个表达式需要用双引号或单引号包围。

示例 2:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
location / {
rewrite ^/test1 /test2;
rewrite ^/test2 /test3 last; # 此处发起新一轮location匹配 uri为/test3
rewrite ^/test3 /test4;
proxy_pass http://www.baidu.com;
}

location = /test2 {
return 200 "/test2";
}

location = /test3 {
return 200 "/test3";
}
location = /test4 {
return 200 "/test4";
}

发送如下请求 curl 127.0.0.1:8080/test1 返回 /test3

当如果将上面的 location / 改成如下代码

1
2
3
4
5
6
7
8
9
location / {
rewrite ^/test1 /test2;
# 此处不会发起新一轮location匹配;当是会终止执行后续rewrite模块指令重写后的 URI 为 /more/index.html
rewrite ^/test2 /more/index.html break;
rewrite /more/index\.html /test4; # 这条指令会被忽略

# 因为 proxy_pass 不是rewrite模块的指令 所以它不会被 break终止
proxy_pass https://www.baidu.com;
}

发送请求 127.0.0.1:8080/test1
代理到 https://www.baidu.com

rewrite 后的请求参数:
如果替换字符串 replacement 包含新的请求参数,则在它们之后附加先前的请求参数。如果你不想要之前的参数,则在替换字符串 replacement 的末尾放置一个问号,避免附加它们。

1
2
# 由于最后加了个 ?,原来的请求参数将不会被追加到 rewrite 之后的 URI 后面*
rewrite ^/users/(.*)$ /show?user=$1? last;

rewrite_log 指令

语法:rewrite_log on | off;
默认值:rewrite_log off;
作用域:httpserverlocationif
功能:开启或关闭以 notice 级别打印 rewrite 处理日志到 error log 文件。

set 指令

语法:set variable value;
默认值:none
作用域:serverlocationif
定义一个变量并赋值,值可以是文本,变量或者文本变量混合体。

uninitialized_variable_warn 指令

作用域:httpserverlocationif
语法:uninitialized_variable_warn on | off;
默认值:uninitialized_variable_warn on
功能:控制是否记录 有关未初始化变量的警告。

一文理清nginx中的location配置

https://segmentfault.com/a/1190000022315733

作者

云鱼Cloudy

发布于

2021-12-15

更新于

2021-12-15

许可协议