今天的内容又是在 Nginx 的学习中非常重要的一块。可以说,只要你是做 PHP 开发的,那么肯定会接触过今天的内容。为什么这么说呢?因为你只要用了 PHP 框架,不管是 TP 还是 Laravel ,都会需要今天学习到的内容来进行相应的配置,实现去除 index.php 之类的功能。另外,包含在这个模块中的 return、set、if 也是我们之前都已经接触过的,特别是 retrun ,几乎每篇文章都用到了。
整个重写模块的命名是 ngx_http_rewrite_module 模块,它用于通过 PCRE 正则表达式更改请求 URI、返回重定向和有条件地选择配置的功能。
今天的内容大部分可以在 server、location 中进行配置,仅有两个指令也可以在 http 下配置。我们今天边学习每个指令,边进行测试。
停止处理当前的 ngx_http_rewrite_module 指令集。
break;
如果在 location 内指定了指令,则在该位置继续对请求进行进一步处理。这个指令会中断请求的处理,就像我们在 PHP 的循环中的 break 一样,直接退出循环,这里就是直接完成请求的处理。
location /breaktest/ {
alias html/;
if ($arg_a) {
break;
}
return 200 aaabbb;
}
上面配置的意思是,当我们直接请求的时候,可以通过 return 返回 aaabbb 这样的字符串。
➜ ~ curl http://192.168.56.88/breaktest/
aaabbb
但如果加上一个 GET 参数 a ,就会显示正常的页面,因为进入到了 if 条件判断中,直接运行了 break 。break 后面的指令代码就不会执行了,也就是不会走 return ,而是直接显示 html 目录下的文件内容。
➜ ~ curl "http://192.168.56.88/breaktest/?a=1"
Welcome to nginx!
Welcome to nginx!123123
If you see this page, the nginx web server is successfully installed and
working. Further configuration is required.
For online documentation and support please refer to
nginx.org.
Commercial support is available at
nginx.com.
Thank you for using nginx.
一般来说,直接使用这个指令的可能比较少,更主要的是和 rewrite 配合,后面我们学习 rewrite 的时候再说,它可以是 rewrite 的一个 flag 标志。
判断指定的条件。
if (condition) { ... }
如果为 true,则执行大括号内指定的此模块指令,并为请求分配 if 指令内的配置。 if 指令中的配置继承自之前的配置级别。
这个就和我们在动态语言中的 if 条件语句是类似的了。不过它的条件规则略有不同,这些条件可以是以下任何一种:
变量名,如果变量的值为空字符串或“0”,则为 false。
使用“=”和“!=”运算符将变量与字符串进行比较。
使用“~”(用于区分大小写的匹配)和“~*”(用于不区分大小写的匹配)运算符将变量与正则表达式匹配。正则表达式可以包含可用于以后在 9 变量中重用的捕获。也可以使用负运算符“!~”和“!~*”。如果正则表达式包含“}”或“;”字符,整个表达式应该用单引号或双引号括起来。
使用“-f”和“!-f”运算符检查文件是否存在。
使用“-d”和“!-d”运算符检查目录是否存在。
使用“-e”和“!-e”运算符检查文件、目录或符号链接是否存在。
使用“-x”和“!-x”运算符检查可执行文件。
我们一个一个来测试。
先进行最简单的测试,我们直接在 server 下配置,这样所有的请求都会走到判断这里。
if ($arg_b = "b") {
return 200 bb;
}
if ($arg_b) {
return 200 b;
}
if ($arg_c != "2"){
return 200 $arg_c;
}
访问页面,如果请求有 GET 参数 b ,就会返回 b 这个字符串;如果 b 的值是 b ,就会返回 bb 这个字符串。如果没有参数 c 或者参数 c 不等于 2 ,就会返回参数 c 的值。这一段大家可以测试一下,我就不放上测试结果啦。
上面的列表中,我们会发现,它有一些特殊的判断符号可以代表特别的意思,-d 表示目录存在,-f 表示文件存在,-e 表示或目录或文件或软链接存在,这三个参数前面加上感叹号就表示取反不存在。
location /iftest1/ {
alias html/;
if (-d "iftest1"){
return 200 iftest1;
}
}
location /iftest2/ {
alias html/;
if (!-d "iftest2"){
return 200 iftest2;
}
}
这两段配置,分别判断当前目录是否存在,其实也就是我们访问的路径 URI 是否存在,第一个会进入到 alias 的 html 中,因为判断条件是目录 iftest1 是否存在,明显这是无法通过的;而第二个则会返回 iftest2 字符串,因为条件判断成功了。
location /iftest3/ {
alias html/;
if (-f index.html){
return 200 iftest3;
}
}
location /iftest4/ {
alias html/;
if (!-f index.html){
return 200 iftest4;
}
}
这两段配置是使用文件判断,大家可以猜猜这里哪个会返回 return 语句的内容。
最后,还有一个 -e 的例子,我们直接使用 $request_filename
变量,如果请求的完整路径文件不存在,就返回 iftest5 。
location /iftest5/ {
if (!-e $request_filename){
return 200 iftest5;
}
}
最后一个是不是好眼熟,又要搬出 TP 文档中的那一段配置了,也就是去 index.php 的那个 Nginx 配置。
# TP6
location / { // …..省略部分代码
if (!-e $request_filename) {
rewrite ^(.*)$ /index.php?s=/$1 last;
}
}
这下是不是非常好理解了?我们通过 !-e 判断,如果访问的路径或文件不存在,就使用 rewrite 重写为 /index.php 文件,并且通过正则表达式将请求完整路径内容放到它的 s 参数中。rewrite 我们下面马上就讲,这一段 rewrite 的意思我们下面也会再说一下。而 Laravel 的配置,和它略有不同,之前在 Nginx学习:FastCGI模块(四)错误处理及其它https://mp.weixin.qq.com/s/XnWng2iDfuEiacOlWsK6cQ 中的 fastcgi_split_path_info 部分也讲过了,不记得的小伙伴可以回去看一下哦。
最后还有一个 -x 和 !-x ,可以用于判断指定的文件或路径是否有 x 这个权限,也就是 Linux 系统中的执行权限。
location /iftest6/ {
if (!-x 2.php){
return 200 iftest6;
}
}
location /iftest7/ {
if (-x 2.php){
return 200 iftest7;
}
}
我们通过 chmod +x /usr/local/nginx/html/2.php
给 2.php 这个文件加上了 x 权限,然后运行上面的测试,大家说说会那一个 location 会输出 return 的内容呢?
停止处理并将指定的代码返回给客户端。
return code [text];
return code URL;
return URL;
非标准代码 444 关闭连接而不发送响应头.
从版本 0.8.42 开始,可以指定重定向 URL(用于代码 301、302、303、307 和 308)或响应正文文本(用于其他代码)。响应正文和重定向 URL 可以包含变量。作为一种特殊情况,可以将重定向 URL 指定为此服务器的本地 URI,在这种情况下,根据请求方案 ($scheme) 以及 server_name_in_redirect 和 port_in_redirect 指令形成完整的重定向 URL。
此外,可以将带有代码 302 的临时重定向 URL 指定为唯一参数。此类参数应以“http://”、“https://”或“$scheme”字符串开头。 URL 可以包含变量。
在 0.7.51 版本之前,只能返回以下代码:204、400、402——406、408、410、411、413、416 和 500——504。代码 307 直到版本 1.1.16 和 1.0.13 才被视为重定向。代码 308 直到版本 1.13.0 才被视为重定向。
这个不多做解释了,我们用得太多了。官网也推荐如果是使用 301 302 之类的跳转,尽量直接使用 return ,因为它和 location 的正则可以进行配合,从而实现大部分 rewrite 的功能。
location ~ /returntest1/(.*)$ {
return 301 /$1;
}
这段配置中,location 通过正则匹配,然后 return 直接 301 去展示 / 目录的内容。后面我们在 rewrite 中也会看到类似的操作。它也可以直接使用一个参数进行 URL 的跳转。
location /returntest2/ {
return "http://www.baidu.com";
}
测试后可以看到,它默认走的就是 302 跳转。这里可以看到,直接跳转我们可以不用 code 参数,注意,只有跳转 URL 时可以不用,直接的字符串打印是需要 code 码的,也就是说,return 后面如果跟着字符串,有 http 这种协议的,就会走默认的 302 跳转,否则代码配置加载也通过不了。大家可以自己试试哦。
另外一个需要注意的,它可以会引起重复重定向的问题。比如这样:
location /returntest3/ {
return 301 /returntest3/;
}
不停的 301 到自己,然后形成死循环,这种情况服务端不会报错,错误日志中不会有记录。客户端浏览器会显示重定向次数过多的错误。
如果指定的正则表达式与请求 URI 匹配,则 URI 将按照替换字符串中的指定进行更改。
rewrite regex replacement [flag];
重头戏来了啊,这个 rewrite 非常强大。这个重写指令按照它们在配置文件中出现的顺序依次执行。可以使用标志终止对指令的进一步处理。如果替换字符串以“http://”、“https://”或“$scheme”开头,则处理停止并将重定向返回给客户端。
可选的标志参数可以是以下之一:
last ,停止处理当前的 ngx_http_rewrite_module 指令集并开始搜索与更改的 URI 匹配的新位置
break,与 break 指令一样,停止处理当前的 ngx_http_rewrite_module 指令集
redirect,返回带有 302 代码的临时重定向;如果替换字符串不以“http://”、“https://”或“$scheme”开头,则使用该字符串
permanent,返回带有 301 代码的永久重定向
如果正则表达式包含“}”或“;”字符,整个表达式应该用单引号或双引号括起来
这个指令很神奇,return 全部都是跳转,但它如果指定的路径不是以 http 这种协议开头的,则会内部再走一次 Nginx 匹配。换句话说,客户端那边看不到 301 或 302 这样的一次跳转请求。
比如我们这样配置一个。
location /rewrite1/ {
rewrite 1.html /index.html;
}
访问 /rewrite1/1.html 时,会返回 html 目录下的 index.html 。客户端也没有任何的跳转信息,就是这一个请求返回的响应。
本身第一个参数就是正则表达式,所以我们也可以这样写,效果和上面的一样。
location /rewrite2/ {
rewrite ^/rewrite2/(.*)$ /$1;
}
将所有 /rewrite2/ 的访问,都转到 / 根目录下,第一个测试是指定文件了,这个测试则是完全的就跟访问 / 路径一样。
外网跳转也是 OK 的。
location /rewrite3/ {
rewrite ^ http://www.baidu.com;
}
全部转到百度去,这里是有 http 协议的啦,所以外网默认就是 302 跳转。
接下来,我们看最最重点的内容,那就是 rewrite 最后可选的 flag 参数。
先来看两个简单的,就是上面最后那两个 flag 配置。其实就是一个表示 301 一个表示 302 。
location /rewrite1/ {
rewrite 1.html /index.html redirect;
}
加上 redirect 后,即使是内部的重写,也会实现成一次 302 跳转。另一个 permanent ,就表示的是 301 跳转。
location /rewrite2/ {
rewrite ^/rewrite2/(.*)$ /$1 permanent;
}
上面我们已经看到了,默认外网是 302 跳转,但也可以指定为 301 跳转,这个大家直接自己试下就知道了。
来了来了,last 和 break ,这俩货没有系统学习之前真的是不太了解。现在咱们就详细地看一下。先准备几个测试的 location 。
location /rewrite4/ {
rewrite ^ /rewrite1.html;
return 200 1;
}
location /rewrite1.html {
rewrite ^ /rewrite2.html;
}
location /rewrite2.html {
rewrite ^ /rewrite3.html;
}
location /rewrite3.html {
return 200 1,2,3html;
}
直接访问 /rewrite4/ 会返回 1 ,这是默认情况下整个 location 内的代码都执行完成了,才会开始 rewrite ,明显 return 的优先级更高一些,它是直接中断的。咱们先看看使用 last 的效果。
rewrite ^ /rewrite1.html last;
返回的结果会走 rewrite ,也就是返回最后的 1,2,3html 这样的内容,其实 last 是中断当前的 location 中的执行,直接就开始 rewrite ,并且一直匹配。接下来再看 break 的作用。
rewrite ^ /rewrite1.html break;
访问路径后,返回的是 404 ,错误日志是这样的。
2022/09/19 09:20:13 [error] 1685#0: *22 open() "/usr/local/nginx/html/rewrite1.html"
看出差别了吧,break 只匹配当前这个 rewrite 的内容,也就是 /rewrite1.html ,即使我们还定义了一个同名的 location ,也不会再去匹配这个 location 里面的内容了。就相当于是只访问这个有 break 的 rewrite 指定的目录或文件,不再走任何 location。同理,如果我们在 /rewrite1.html 中定义 break :
location /rewrite1.html {
rewrite ^ /rewrite2.html break;
}
那么也最终会去找 /rewrite2.html 这个文件。
2022/09/19 09:22:25 [error] 1716#0: *24 open() "/usr/local/nginx/html/rewrite2.html"
最后,上面这四个 flag 标志可以一起定义吗?
rewrite ^ /rewrite1.html break last;
不行的,在这个配置指令的文档定义中就看出来了,flag 只能有一个,没有 ... 之类的,所以像上面的配置会报错。
nginx: [emerg] invalid number of arguments in "rewrite" directiv
rewrite 还可以直接在 server 下配置。
rewrite ^/(rewrite5)/(.*)$ /$1/test/$2;
location /rewrite5/ {
return 200 $uri;
}
这一段的意思是将 /rewrite5 转换成 /rewrite5/test/xxx 这样的形式。
➜ ~ curl http://192.168.56.88/rewrite5/
/rewrite5/test/
➜ ~ curl http://192.168.56.88/rewrite5/aabb/1.html
/rewrite5/test/aabb/1.html
这里需要注意的是,如果在 location 中这样写,也会引起无限循环重写。
location /rewrite6/ {
rewrite ^/(rewrite6)/(.*)$ /$1/test/$2;
}
很好理解,它会一直不停地进入到 /rewrite6 中,然后不停地加 /test ,报错的内容就像下面这样。这个地方是会显示在报错日志中的,因为它有个上限是十次。这里和 return 不同的地方在于,return 是走 301 或 302 的,它会响应状态码和 Location 并由浏览器发送请求,所以服务端这边理论上是没错的,只是客户端报错。而 rewrite 在没有使用 permanent 或 redirect 的情况下,是内部代码在循环查找,所以是服务端的逻辑错误,就会将日志记录到 error_log 中。
2022/09/19 09:30:04 [error] 1744#0: *28 rewrite or internal redirection cycle while processing "/rewrite6/test/test/test/test/test/test/test/test/test/test/test/"
最后我们再来看一下 TP 配置中的 rewrite 部分。
rewrite ^(.*)$ /index.php?s=/$1 last;
匹配的内容前面已经解释过了,最后的 last 就表示中断当前 location 的执行,开始完全的匹配。
在通知级别启用或禁用将 ngx_http_rewrite_module 模块指令处理结果记录到 error_log 中。
rewrite_log on | off;
默认值 off ,也可以在 http 上进行配置,配置成 on 之后,会在 error_log 的 notice 级别上生成两条如下的日志。
2022/09/19 10:49:58 [notice] 1967#0: *36 "1.html" matches "/rewrite1/1.html", client: 192.168.56.1, server: core.nginx.test, request: "GET /rewrite1/1.html HTTP/1.1", host: "192.168.56.88", referrer: "http://xxx"
2022/09/19 10:49:58 [notice] 1967#0: *36 rewritten data: "/index.html", args: "", client: 192.168.56.1, server: core.nginx.test, request: "GET /rewrite1/1.html HTTP/1.1", host: "192.168.56.88", referrer: "http://xxx"
一般来说不用打开,会增加磁盘写入操作。
设置指定变量的值。
set $variable value;
该值可以包含文本、变量及其组合。
之前我们其实也用过了,在 map 相关的配置中也讲过一点,现在就来简单测试一下。
location /settest1/ {
set $a 1;
set $b aabb;
set $c a1b2$uri;
set $d 'd1 $uri';
return 200 $a,$b,$c,$d;
}
第一个是变量名,第二个参数可以是数字,可以是字符串,也可以是它们的组合。这里比较需要关注的是如果要输出空格,一定是带引号的这种形式。
那么能不能覆盖已有的 Nginx 变量呢?
location /settest2/ {
#set $uri 123123;
set $arg_param bbb;
return 200 $uri,$arg_param;
}
$uri
这种是不行的,但是 $arg_[name]
这类可以外部接收的变量是可以的。如果尝试设置 $uri
,会报出这样的错误。
nginx: [emerg] the duplicate "uri" variable in /etc/nginx/article.server.d/33.conf:82
而即使我们的 GET 参数中带了 param 这个参数,最终显示的结果也是 bbb 。另外,变量和字符串不能这样拼接。
set $arg_param $arg_parambbb;
但是可以这样。
set $arg_param bbb$arg_param;
第一种拼接方式,会让 Nginx 认为整个 $arg_parambbb
是一个完整的变量名,而第二个则会区分开。要想要变量在前面,需要给变量名加上花括号。
set $arg_param ${arg_param}bbb;
这里的字符串拼接规则适用于全部的可以使用字符串的地方,比如 return 。
控制是否记录有关未初始化变量的警告。
uninitialized_variable_warn on | off;
默认 on ,可以配置到 http 。没试出来效果。
break、if、return、rewrite 和 set 指令按以下顺序处理:在服务器级别指定的该模块的指令按顺序执行。前面说过 return 和 rewrite 的问题,不带 flag 的rewrite 是不会中断执行的,所以如果 rewrite 有了 flag 参数,就不会走后面的 return 了。
循环问题:
根据请求 URI 搜索位置
在找到的位置内指定的该模块的指令按顺序执行
如果请求 URI 被重写,则循环重复,但不超过 10 次
最后,ngx_http_rewrite_module 模块指令的执行原理就是在配置阶段这些指令会被编译成内部指令,在请求处理期间被解释。而解释器是一个简单的虚拟堆栈机器。本身 Nginx 是 C/C++ 写的,是静态语言,但它又针对 ngx_http_rewrite_module 做了一个简单的解释器,就让这些配置指令有了动态语言的特点,可以随时修改执行。
这篇文章的内容其实非常常用,但放到这么后面也是因为咱是按文档顺序在学习嘛。不过这些内容,可以列为和 server、location、proxy、fastcgi 相同级别的重要内容。是我们在学习 Nginx 中必须掌握的内容之一,非常重要。另外还有一个重点模块是什么呢?那就是服务器组 upstream 模块,这一部分我们后面也会学到,不要着急哦,循序渐进,松驰有度地学习效率才更高。
参考文档:
http://nginx.org/en/docs/http/ngx_http_rewrite_module.html