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

文章目录
    分类:存储运维 标签:Nginx

    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 指令集。

    	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"
    <!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

    判断指定的条件。

    if (condition) { ... }

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


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

    • 变量名,如果变量的值为空字符串或“0”,则为 false。

    • 使用“=”和“!=”运算符将变量与字符串进行比较。

    • 使用“~”(用于区分大小写的匹配)和“~*”(用于不区分大小写的匹配)运算符将变量与正则表达式匹配。正则表达式可以包含可用于以后在 $1..$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

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

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

    rewrite

    如果指定的正则表达式与请求 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 参数。

    301 302 跳转

    先来看两个简单的,就是上面最后那两个 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

    来了来了,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

    在 server 下使用

    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 的执行,开始完全的匹配。

    rewrite_log

    在通知级别启用或禁用将 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

    设置指定变量的值。

    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

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

    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

    视频链接

    微信文章地址:https://mp.weixin.qq.com/s/K96frxFl1bTQJEavrx3xow

    B站视频地址:https://www.bilibili.com/video/BV1TG411Y7DN/

    搜索
    关注