Nginx Rewrite
网站在使用 Nginx 时都会进行个性化配置满足自己的业务需要,而 URL 重写几乎是每个网站都必做的事情,Nginx 的 URL 重写规则不像 Apache 那样简单直接,逻辑相对要复杂一些,本文将通过例子的方式帮助大家理解 Nginx rewrite 原理,希望能对您有些启发。
Nginx 中重定向的多种方式
外部重定向
return 指令返回 301 或 302(return 也可以返回其他状态码),可以放在 server 或 location 块中。例如:
1 | return 301 https://www.mi.com; |
还可以使用 rewrite 指令,例如:
1 | rewrite ^/(.*)$ http://www.mi.com/$1; |
内部重定向
return + error_page 指令的组合,或 try_files 指令和 rewrite 指令,非常灵活。
本文主要讲解 rewrite 的工作原理,其他指令的使用方法大家可以自行查阅 Nginx 官网。在使用 Nginx 的 rewrite 指令时,flag 可以设置为 last 和 break,这两个 flag 很容易混淆,后面我们会比较这两个 flag 的区别,下面通过示例我们来认识一下 rewrite 指令。
rewrite 语法
1 | Syntax: rewrite regex replacement [flag]; |
regex: 对请求的 URI 做正则匹配
replacement:目标 uri 匹配成功后替换的 url
可以使用的 flag 有以下 4 个(flag 也可以为空):
redirect:返回 302 临时重定向,客户端地址栏会显示跳转后的地址;
permanent:返回 301 永久重定向,客户端地址栏会显示跳转后的地址;
last:内部重定向,停止处理后续 rewrite 模块中的指令(客户端无感知);
break:内部重定向,停止处理后续 rewrite 模块中的指令(客户端无感知)。
NOTE:
- regex 匹配的是 uri,不包含 hostname 和 query string,默认 query string 是被追加到 replacement 末尾,如果不希望在末尾追加请求的 query string,可以在 replacement 的末尾加一个 “?”。
1 | server { |
如果 replacement 是以 “http://“,”https://“ 或 $scheme” 开始的字符串,那么 rewrite 指令停止后面的处理,直接返回给客户端,没有指定 flag 时,与 redirect 效果相同。
没有 flag 的 rewrite 指令根据出现的顺序执行,flag 可以控制指令的执行顺序。
在配置中开启 rewrite_log 指令,日志文件中会记录 rewrite 的匹配过程,有助于调试 rewrite 问题。
Nginx 请求处理流程
在讲 rewrite 前我们先来简单了解下 Nginx 请求处理流程,为什么需要了解请求处理流程呢?因为 rewrite 操作与其中几个 phase 关系很密切,熟悉了请求处理流程,理解 rewrite 执行逻辑就会很容易。在 Nginx 内部将请求处理划分为 11 个 phase,每个 phase 会执行对应的 handler,这里我们不打算逐个进行讲解。在 11 个 phase 中与 rewrite 指令逻辑有关的只有 4 个,所以在本文我们主要关注 SERVER_REWRITE、FIND_CONFIG、REWRITE 和 POST_REWRITE 这四个 phase。
首先我们要清楚的是:
server 块中的 rewrite 模块指令在 SERVER_REWRITE 阶段解析;
location 块中的 rewrite 模块指令在 REWRITE 阶段解析;
SERVER_REWRITE - 请求到达后首先处理这个阶段的 rewrite 指令操作
FIND_CONFIG - 根据 SERVER_REWRITE 阶段得到的 uri 查找 location
REWRITE - 确定 location 后执行 locaton 中 rewrite 操作
POST_REWRITE - 根据上一阶段的 uri 重写结果做决策,可能跳回 FIND_CONFIG 阶段重新查找 location,也可能继续执行后边的 phase。例如:在 location 中配置了 rewrite 指令并且指定 flag=break,执行完本条 rewrite 终止后边的 rewrite 匹配,然后执行 PREACCESS 阶段中的 handler。同样的场景下 flag=last,执行完本条 rewrite 终止后边的 rewrite 匹配,然后跳到 FIND_CONFIG 阶段再次查找 location。未指定 flag 的情况与 flag=last 类似,唯一区别是在同一层级中未指定 flag 的 rewrite 语句不会终止后续的 rewrite 匹配。
通过例子理解 rewrite 指令
1.未指定 flag
未指定 flag 的 rewrite 会按照出现顺序进行匹配,server 块中 rewrite 匹配完以后根据改写的 uri 查找 location,然后再匹配 location 中的 rewrite,location 中的 rewrite 指令匹配成功后会再次查找 location。
1 | server { |
说明:
匹配第一条 rewrite 成功,uri 被改写为 / mi_one/mi_zero/hello,没有指定 flag 的 rewrite 继续匹配后面的 rewrite
匹配第二条 rewrite 成功,此时 uri 被改写为 / mi_two/mi_zero/hello
查找 location,mi_two 被确定为最终的 location
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19server {
rewrite ^/(.*)$ /mi_one/$1;
location / {
echo "This is default location";
}
location /mi_one/ {
rewrite ^/mi_one/(.*)$ /mi_two/$1;
echo "This is mi_one location";
}
location /mi_two/ {
echo "This is mi_two location";
}
}
$ curl http://localhost/hello
This is mi_two location
说明:
匹配 server 块中的 rewrite 成功,uri 被改写为 / mi_one/hello
server 块中只有一条 rewrite 指令,开始查找 location
location mi_one 被找到,开始匹配 location 中 rewrite
location 中的 rewrite 匹配成功,uri 被改写为 / mi_two/hello
再次查找 location,mi_two 被确定为最终使用的 location
再来看一个例子:
1 | server { |
说明:
匹配第一条 rewrite 成功,由于 replacement 是以 http:// 开始的字符串,所以 rewrite 指令直接返回给客户端 302,并且停止匹配后续的 rewrite。
2.flag 指定为 redirect
指定 flag 为 redirect 时,rewrite 匹配成功后直接返回给客户端 302,不会继续匹配后续的 rewrite。
1 | server { |
再来看一个例子:
1 | server { |
3.flag 指定为 permanent
指定 flag=permanent 时,与 redirect 效果相同,唯一的区别 http code 返回 301。
4.flag 指定为 last
rewrite 的 last 和 break 这两个 flag 使用场景很多并且也很容易混淆,他们的共同点都会停止当前层级后续的 rewrite 匹配,区别需要分两种情况:第一种使用在 server block 中,last 和 break 没有区别。第二种使用在 location block 中,last 会根据改写的 uri 重新查找 location,break 不会重新查找 location,而是在当前 location 中执行后续的指令。(注:last 和 break 不仅停止 rewrite 的匹配,同时还会停止 Nginx rewrite 模块中其他指令的执行,例如:set、return 指令)
1 | server { |
说明:
- 匹配第一条 rewrite 成功,uri 被改写为 / mi_two/hello,由于 flag 指定为 last 会停止后续的 rewrite 的匹配(仅停止 server block 中的 rewrite 匹配),所以会根据改写的 uri 查找 location
再来看一个 last 在 location block 中使用的例子:
1 | server { |
说明:
匹配 server block 中第一条 rewrite 成功,uri 被改写为 / mi_one/hello
查找 location,mi_one 被确定为使用的 location
匹配 location block 中的 rewrite,location 中的第一条 rewrite 匹配成功,uri 被改写为 / mi_three/mi_one/hello,由于 flag 指定为 last,所以停止 location 中后续的 rewrite 匹配,此时再根据 uri=/mi_three/mi_one/hello 查找 location,最终 mi_three 被确定为使用的 location
5.flag 指定为 break
1 | server { |
说明:
匹配 server block 中第一条 rewrite 成功,uri 被改写为 / mi_two/hello,由于 flag 指定为 break 所以会停止 server block 中后续的 rewrite 匹配,根据 uri=/mi_two/hello 查找 location,最终 mi_two 被确定为使用的 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
27server {
rewrite ^/(.*)$ /mi_one/$1;
location / {
echo "This is default location";
}
location /mi_one/ {
rewrite ^/(.*)$ /mi_three/$1 break;
rewrite ^/mi_two/(.*)$ /;
echo "This is mi_one location";
echo "uri: ${uri}";
}
location /mi_two/ {
echo "This is mi_two location";
}
location /mi_three/ {
echo "This is mi_three location";
}
}
$ curl http://localhost/hello
This is mi_one location
uri: /mi_three/mi_one/hello说明:
匹配 server block 中第一条 rewrite 成功,uri 被改写为 / mi_one/hello
根据 uri=/mi_one/hello 查找 location,mi_one 被确定为使用的 location
匹配 location block 中的 rewrite,location 中的第一条 rewrite 匹配成功,uri 被改写为 / mi_three/mi_one/hello,由于 flag 指定为 break,所以停止 location 中后续的 rewrite 匹配,并且把当前 location 作为最终使用的 location,不会重新查找 location(last 会继续查找 location)
总结
在 Nginx 的配置中可以实现简单的编程,理解起来相对有点难度,通过阅读此文希望能对你有些启发,能够根据项目需求可以配置更复杂的 rewrite 规则。想要更深入的理解 rewrite,还需要大家自己动手实践。
FYI
https://github.com/agile6v/awesome-nginx
http://nginx.org/en/docs/http/ngx_http_rewrite_module.html
https://www.nginx.com/blog/creating-nginx-rewrite-rules/
https://www.nginx.com/blog/converting-apache-to-nginx-rewrite-rules
http://www.thegeekstuff.com/2017/08/nginx-rewrite-examples/
http://winginx.com/en/htaccess
https://w3techs.com/technologies/overview/web_server/all
http://nginx.org/en/docs/dev/development_guide.html#httpphases