【Nginx33】Nginx学习:重写更改请求模块

2023-10-23 17:37:52 浏览数 (2)

Nginx学习:重写更改请求模块

今天的内容又是在 Nginx 的学习中非常重要的一块。可以说,只要你是做 PHP 开发的,那么肯定会接触过今天的内容。为什么这么说呢?因为你只要用了 PHP 框架,不管是 TP 还是 Laravel ,都会需要今天学习到的内容来进行相应的配置,实现去除 index.php 之类的功能。另外,包含在这个模块中的 return、set、if 也是我们之前都已经接触过的,特别是 retrun ,几乎每篇文章都用到了。

整个重写模块的命名是 ngx_http_rewrite_module 模块,它用于通过 PCRE 正则表达式更改请求 URI、返回重定向和有条件地选择配置的功能。

今天的内容大部分可以在 server、location 中进行配置,仅有两个指令也可以在 http 下配置。我们今天边学习每个指令,边进行测试。

break

停止处理当前的 ngx_http_rewrite_module 指令集。

代码语言:javascript复制
 break;

如果在 location 内指定了指令,则在该位置继续对请求进行进一步处理。这个指令会中断请求的处理,就像我们在 PHP 的循环中的 break 一样,直接退出循环,这里就是直接完成请求的处理。

代码语言:javascript复制
location /breaktest/ {
  alias html/;

  if ($arg_a) {
    break;
  }
  return 200 aaabbb;
}

上面配置的意思是,当我们直接请求的时候,可以通过 return 返回 aaabbb 这样的字符串。

代码语言:javascript复制
➜  ~ curl http://192.168.56.88/breaktest/
aaabbb

但如果加上一个 GET 参数 a ,就会显示正常的页面,因为进入到了 if 条件判断中,直接运行了 break 。break 后面的指令代码就不会执行了,也就是不会走 return ,而是直接显示 html 目录下的文件内容。

代码语言:javascript复制
➜  ~ curl "http://192.168.56.88/breaktest/?a=1"
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
<style>
html { color-scheme: light dark; }
body { width: 35em; margin: 0 auto;
font-family: Tahoma, Verdana, Arial, sans-serif; }
</style>
</head>
<body>
<h1>Welcome to nginx!123123</h1>
<p>If you see this page, the nginx web server is successfully installed and
working. Further configuration is required.</p>

<p>For online documentation and support please refer to
<a href="http://nginx.org/">nginx.org</a>.<br/>
Commercial support is available at
<a href="http://nginx.com/">nginx.com</a>.</p>

<p><em>Thank you for using nginx.</em></p>
</body>
</html>

一般来说,直接使用这个指令的可能比较少,更主要的是和 rewrite 配合,后面我们学习 rewrite 的时候再说,它可以是 rewrite 的一个 flag 标志。

if

判断指定的条件。

代码语言:javascript复制
if (condition) { ... }

如果为 true,则执行大括号内指定的此模块指令,并为请求分配 if 指令内的配置。 if 指令中的配置继承自之前的配置级别。

这个就和我们在动态语言中的 if 条件语句是类似的了。不过它的条件规则略有不同,这些条件可以是以下任何一种:

  • 变量名,如果变量的值为空字符串或“0”,则为 false。
  • 使用“=”和“!=”运算符将变量与字符串进行比较。
  • 使用“~”(用于区分大小写的匹配)和“~*”(用于不区分大小写的匹配)运算符将变量与正则表达式匹配。正则表达式可以包含可用于以后在
1..

9 变量中重用的捕获。也可以使用负运算符“!~”和“!~*”。如果正则表达式包含“}”或“;”字符,整个表达式应该用单引号或双引号括起来。

  • 使用“-f”和“!-f”运算符检查文件是否存在。
  • 使用“-d”和“!-d”运算符检查目录是否存在。
  • 使用“-e”和“!-e”运算符检查文件、目录或符号链接是否存在。
  • 使用“-x”和“!-x”运算符检查可执行文件。

我们一个一个来测试。

变量名和等号

先进行最简单的测试,我们直接在 server 下配置,这样所有的请求都会走到判断这里。

代码语言:javascript复制
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 表示或目录或文件或软链接存在,这三个参数前面加上感叹号就表示取反不存在。

代码语言:javascript复制
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 字符串,因为条件判断成功了。

代码语言:javascript复制
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 。

代码语言:javascript复制


location /iftest5/ {
 if (!-e $request_filename){
  return 200 iftest5;
 }
}

最后一个是不是好眼熟,又要搬出 TP 文档中的那一段配置了,也就是去 index.php 的那个 Nginx 配置。

代码语言:javascript复制
# 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 系统中的执行权限。

代码语言:javascript复制
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

停止处理并将指定的代码返回给客户端。

代码语言:javascript复制
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 的功能。

代码语言:javascript复制
location ~ /returntest1/(.*)$ {
   return 301 /$1;
}

这段配置中,location 通过正则匹配,然后 return 直接 301 去展示 / 目录的内容。后面我们在 rewrite 中也会看到类似的操作。它也可以直接使用一个参数进行 URL 的跳转。

代码语言:javascript复制
location /returntest2/ {
 return "http://www.baidu.com";
}

测试后可以看到,它默认走的就是 302 跳转。这里可以看到,直接跳转我们可以不用 code 参数,注意,只有跳转 URL 时可以不用,直接的字符串打印是需要 code 码的,也就是说,return 后面如果跟着字符串,有 http 这种协议的,就会走默认的 302 跳转,否则代码配置加载也通过不了。大家可以自己试试哦。

另外一个需要注意的,它可以会引起重复重定向的问题。比如这样:

代码语言:javascript复制
location /returntest3/ {
 return 301 /returntest3/;
}

不停的 301 到自己,然后形成死循环,这种情况服务端不会报错,错误日志中不会有记录。客户端浏览器会显示重定向次数过多的错误。

rewrite

如果指定的正则表达式与请求 URI 匹配,则 URI 将按照替换字符串中的指定进行更改。

代码语言:javascript复制
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 这样的一次跳转请求。

比如我们这样配置一个。

代码语言:javascript复制
location /rewrite1/ {
   rewrite 1.html /index.html;
}

访问 /rewrite1/1.html 时,会返回 html 目录下的 index.html 。客户端也没有任何的跳转信息,就是这一个请求返回的响应。

本身第一个参数就是正则表达式,所以我们也可以这样写,效果和上面的一样。

代码语言:javascript复制
location /rewrite2/ {
 rewrite ^/rewrite2/(.*)$ /$1;
}

将所有 /rewrite2/ 的访问,都转到 / 根目录下,第一个测试是指定文件了,这个测试则是完全的就跟访问 / 路径一样。

外网跳转也是 OK 的。

代码语言:javascript复制
location /rewrite3/ {
 rewrite ^ http://www.baidu.com;
}

全部转到百度去,这里是有 http 协议的啦,所以外网默认就是 302 跳转。

接下来,我们看最最重点的内容,那就是 rewrite 最后可选的 flag 参数。

301 302 跳转

先来看两个简单的,就是上面最后那两个 flag 配置。其实就是一个表示 301 一个表示 302 。

代码语言:javascript复制
location /rewrite1/ {
   rewrite 1.html /index.html redirect;
}

加上 redirect 后,即使是内部的重写,也会实现成一次 302 跳转。另一个 permanent ,就表示的是 301 跳转。

代码语言:javascript复制
location /rewrite2/ {
 rewrite ^/rewrite2/(.*)$ /$1 permanent;
}

上面我们已经看到了,默认外网是 302 跳转,但也可以指定为 301 跳转,这个大家直接自己试下就知道了。

last 与 break

来了来了,last 和 break ,这俩货没有系统学习之前真的是不太了解。现在咱们就详细地看一下。先准备几个测试的 location 。

代码语言:javascript复制
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 的效果。

代码语言:javascript复制
rewrite ^ /rewrite1.html last;

返回的结果会走 rewrite ,也就是返回最后的 1,2,3html 这样的内容,其实 last 是中断当前的 location 中的执行,直接就开始 rewrite ,并且一直匹配。接下来再看 break 的作用。

代码语言:javascript复制
rewrite ^ /rewrite1.html break;

访问路径后,返回的是 404 ,错误日志是这样的。

代码语言:javascript复制
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 :

代码语言:javascript复制
location /rewrite1.html {
   rewrite ^ /rewrite2.html break;
}

那么也最终会去找 /rewrite2.html 这个文件。

代码语言:javascript复制
2022/09/19 09:22:25 [error] 1716#0: *24 open() "/usr/local/nginx/html/rewrite2.html"

最后,上面这四个 flag 标志可以一起定义吗?

代码语言:javascript复制
rewrite ^ /rewrite1.html break last;

不行的,在这个配置指令的文档定义中就看出来了,flag 只能有一个,没有 ... 之类的,所以像上面的配置会报错。

代码语言:javascript复制
nginx: [emerg] invalid number of arguments in "rewrite" directiv

在 server 下使用

rewrite 还可以直接在 server 下配置。

代码语言:javascript复制
rewrite ^/(rewrite5)/(.*)$ /$1/test/$2;

location /rewrite5/ {
   return 200 $uri;
}

这一段的意思是将 /rewrite5 转换成 /rewrite5/test/xxx 这样的形式。

代码语言:javascript复制
➜  ~ 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 中这样写,也会引起无限循环重写。

代码语言:javascript复制
location /rewrite6/ {
   rewrite ^/(rewrite6)/(.*)$ /$1/test/$2;
}

很好理解,它会一直不停地进入到 /rewrite6 中,然后不停地加 /test ,报错的内容就像下面这样。这个地方是会显示在报错日志中的,因为它有个上限是十次。这里和 return 不同的地方在于,return 是走 301 或 302 的,它会响应状态码和 Location 并由浏览器发送请求,所以服务端这边理论上是没错的,只是客户端报错。而 rewrite 在没有使用 permanent 或 redirect 的情况下,是内部代码在循环查找,所以是服务端的逻辑错误,就会将日志记录到 error_log 中。

代码语言:javascript复制
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 部分。

代码语言:javascript复制
rewrite  ^(.*)$  /index.php?s=/$1  last;

匹配的内容前面已经解释过了,最后的 last 就表示中断当前 location 的执行,开始完全的匹配。

rewrite_log

在通知级别启用或禁用将 ngx_http_rewrite_module 模块指令处理结果记录到 error_log 中。

代码语言:javascript复制
rewrite_log on | off;

默认值 off ,也可以在 http 上进行配置,配置成 on 之后,会在 error_log 的 notice 级别上生成两条如下的日志。

代码语言:javascript复制
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

设置指定变量的值。

代码语言:javascript复制
set $variable value;

该值可以包含文本、变量及其组合。

之前我们其实也用过了,在 map 相关的配置中也讲过一点,现在就来简单测试一下。

代码语言:javascript复制
location /settest1/ {
  set $a 1;
  set $b aabb;
  set $c a1b2$uri;
  set $d 'd1 $uri';

  return 200 $a,$b,$c,$d;
}

第一个是变量名,第二个参数可以是数字,可以是字符串,也可以是它们的组合。这里比较需要关注的是如果要输出空格,一定是带引号的这种形式。

那么能不能覆盖已有的 Nginx 变量呢?

代码语言:javascript复制
location /settest2/ {
 #set $uri 123123;
 set $arg_param bbb;
 return 200 $uri,$arg_param;
}

uri 这种是不行的,但是 arg_[name] 这类可以外部接收的变量是可以的。如果尝试设置

代码语言:javascript复制
nginx: [emerg] the duplicate "uri" variable in /etc/nginx/article.server.d/33.conf:82

而即使我们的 GET 参数中带了 param 这个参数,最终显示的结果也是 bbb 。另外,变量和字符串不能这样拼接。

代码语言:javascript复制
set $arg_param $arg_parambbb;

但是可以这样。

代码语言:javascript复制
set $arg_param bbb$arg_param;

第一种拼接方式,会让 Nginx 认为整个 $arg_parambbb 是一个完整的变量名,而第二个则会区分开。要想要变量在前面,需要给变量名加上花括号。

代码语言:javascript复制
set $arg_param ${arg_param}bbb;

这里的字符串拼接规则适用于全部的可以使用字符串的地方,比如 return 。

uninitialized_variable_warn

控制是否记录有关未初始化变量的警告。

代码语言:javascript复制
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

0 人点赞