Nginx “If Is Evil“详解,以及if使用须知

阅读这篇文章前,可以先看看我的另外一篇文章:《Nginx 介绍,以及Nginx配置指令执行的顺序 11 个阶段》https://blog.csdn.net/zzhongcy/article/details/86086369

 

       nginx if 语句其实是 rewrite的一部分,我们在配置nginx时,需要明白if的一些特点。 这里转载了一部分网上的文章,自己再加入了一些配置用例进行说明,希望对大家有帮助。

1. 前言

       有关被 nginx 官方自我批判的 if 语句,《If Is Evil》—— 着实应成为每位开发者初次使用 nginx if 之前必读的文章。

       即使笔者一年前就开始接触使用 nginx 和 if 的组合做 url 跳转,但涉及的功能太简单,一直没能见识到它的真面目。直到近期在多个 if 内处理 proxy_pass 时,噩梦果真就降临了……希望大家仔细阅读上面的文章,引以为戒。

       如果想快速知道为什么 “if is evil”,以及如何避免被坑,可以向下阅读一探究竟。

2. 介绍

       《If Is Evil》这些例子描述的问题有些是 Nginx 的固有逻辑,有些己经被修正了,有些依然如旧。 本文先对rewrite 模块的实现进行总体剖析, 然后再从 Nginx 源代码的角度对这 些例子对应的代码实现一一进行分析。

2.1 几点事实

  • 从 Nginx Configuration Inheritance Model 这篇文章中我们知道, rewrite 模块提供配置指令被称为 action directives,下一层级的 location 中不会从上一级 location 继承这些配置指令。从 Nginx 源代码里的 ngx_http_rewrite_merge_loc_conf 函数中我们能找到证据。

  • server 作用域中的 rewrite 模块指令也不会向下传递到 location 作 用域,但是这些指令会在SERVER_REWRITE 阶段 (先于 REWRITE 阶段) 被执行。 也就是说,server 作用域的 rewrite 模块指令和 location 作用域的 rewrite 模块指令都会被 Nginx 执行。

  • rewrite 模块提供的指令的执行顺序和其在配置文件中的定义顺序一致。

  • if 指令在 Nginx 内部创建了一个无名 location,if 条件为真时,Nginx 使用 这个无名 location作用域的配置处理当前请求

  • POST REWRITE 是 Nginx 内部定义的阶段,通过检查请求 uri 是否被 rewrite 模块修改 (r->uri_changed),判断是否需要使用修改后的 uri 重新开始 FIND CONFIG 以重新匹配合适的location。比如在 location 中有配置如 rewrite ... last; 且 rewrite 成功和请求 uri 匹配成功时。

2.2 rewrite 模块

       rewrite 模块是一个 phase handler, 其初始化函数 ngx_http_rewrite_init 在 SERVER REWRITE 和 REWRITE 阶段 注册了相同的处理函数 ngx_http_rewrite_handler。其中,SERVER REWRITE 阶段 的处理函数用于执行 server 作用域中的 rewrite 模块指令,而REWRITE 阶 段的处理函数用于执行 location 和 if 作用域的 rewrite 模块指令。

rewrite 模块文档 中提到:

The ngx_http_rewrite_module module directives are compiled at the configuration stage into internal instructions that are interpreted during request processing. An interpreter is a simple virtual stack machine.

       这个 ”虚拟栈机“ (Virtual stack machine) 就是我们在配置变量 一文中分析的 Nginx 的命令解释器ngx_http_script_engine_t。Nginx 将包含变量的 配置指令 (directive) 编译为可以在运行对变量进行求值的命令 (instruction,实际上 是一系统预先定义的函数),然后在 Nginx 处理请求的过程中执行这些命令对变量求值。

       rewrite 模块提供的配置指令基本都和变量有关系,if 除了使用变量之外,还需 要根据变量值执行不同的逻辑 (使用不同的 location 继续处理请求),所以这个模块 也是 Nginx 提供的命令解释器的大用户。rewrite 模块被编译后的命令保存在 ngx_http_rewrite_loc_conf_t::codes 字段中。

typedef struct {  
  ngx_array_t *codes;  
  ngx_uint_t stack_size;  
  ... 
} ngx_http_http_rewrite_loc_conf_t; 

       接下来,我们选取两个配置指令 break 和 if,从配置解析和请求处理两个阶段对相 关代码进行分析。

2.3 break

       Nginx 大部分模块提供的配置指令的生效顺序和其在配置文件中的顺序并没有什么关系, 比如proxy 模块。但是有些模块提供的配置指令像编程语言的语句一样,生效顺序 又和配置顺序一致,比如 http core 模块提供的使用了正则表达式的 location 和本文的主角 rewrite 模块提供的 if,break, return 和 rewrite 指令。

       break 指令用于在运行时中断 rewrite 模块指令编译后的命令执行流程,也就是 说,break 之前的命令执行完毕后,就会结束当前的 SERVER REWRITE 或者 REWRITE 阶段,随后 Nginx 进入下一个阶段 (PHASE)。

       在实际生成环境中,单独使用 break 并没有太大的实际意义。但是如果和 if 指令 结合使用,那作用就大了。

2.4 if

       Nginx 在运行过程中对 if 指令定义的表达式求值:如果表达式值为 true,Nginx 使用 if 创建的无名 location {} 的配置处理该请求;如果表达式值为 false, Nginx 跳过 if {},继续执行后面的 rewrite 模块指令。

先来大概了解下 if 的用法:

  • if 指令语法是:if (condition) { ... };
  • if 指令只能用于 server {} 和 location {} 中;
  • if 指令的 condition 可以是:
    • 单个变量 - 变量值为空字符串或者 "0" 时表达式为 false,其余情形为 true;
    • 一元运算符和一个变量 - 支持的一元运算符有 -f, !-f, -d, !-d, -e, !-e, -x, !-x。这些运算符的具体作用参见官方文档;
    • 二元运算符和两个变量 (或一个变量和一个常量,变量必须放在运算符前面) - =, !=, ~, ~*, !~,!~*。这些运算符的具体作用参见官方文档;

    if 指令的配置解析和向运行时命令的编译过程,及运行时命令的解释执行,比上文分 析的 break要复杂很多,和上文对 break 的分析类似,我们依然按静态配置解析和 运行时命令执行两个阶段,对 if 指令的代码实现进行分析。

在罗列代码之前,我们先来给出几个结论:

   (1) if {} 作用域里的 rewrite 模块指令和它所在的 server 或者 location 作用域里的 rewrite 模块指令共用编译后的命令存储空间,即 ngx_http_rewrite_loc_conf_t::codes。这个设定最早可追溯到 Nginx 早期的 0.17 RELEASE 版本:

*) Changes: the ngx_http_rewrite_module was rewritten from the scratch. Now it is possible to redirect, to return the error codes, to check the variables and referers. The directives can be used inside locations. The redirect directive was canceled.

       我猜测,Nginx 之所以这样实现是为了简化逻辑。如果让 if {} 创建的无名 location {} 拥有自己独立的命令数组,那么进入了这个无名 location{} 后,如果需要执行其中的 rewrite 模块指令对应的命令,势必需要再次调用 ngx_http_rewrite_handler 或 者初始化一个新的解释器。而如果在 if {} 配置的还有其它 rewrite 模块指令, 那么在执行完无名 location 的命令后,还需要再次恢复原先的解释器环境。这个过程 增加了代码实现的复杂度。

上面的描述引出了另外一个结论:

      (2) if 条件为 true 的情况下,Nginx 虽然把请 求和无名location {} 关联了起来,但是由于当前依然处于 REWRITE 阶段, if {} 后面的 rewrite 模块指令依然需要继续执行。"What happens in Vegas stays in Vegas"。在 REWRITE 阶段定义的指令,在此阶段执行完毕后,Nginx 才 开始执行下一个阶段的指令。

 

3 Why《If Is Evil》

nginx if 语句其实是 rewrite 模块的一部分,尽管在非 rewrite 环境下也可以用,但必须要意识到这是一种误用,因此才能理解为何它会引发众多意料之外的问题。

3.1 How

下面的示例代码引自 If Is Evil 的 Example 部分,用 30 秒即可浏览大部分 evil 场景。

# 这里收集了一些出人意料的坑爹配置来展示 location 中的 if 指令是万恶的
 
# 只有第二个 header 才会在响应中展示
# 这不是Bug, 只是他的处理流程如此
location /only-one-if {
    set $true 1;
    if ($true) {
        add_header X-First 1;
    }
    if ($true) {
        add_header X-Second 2;
    }
    return 204;
}
 
# 因为if, 请求会在未改变uri的情况下下发送到后台的 '/'
location /proxy-pass-uri {
    proxy_pass http://127.0.0.1:8080/;
    set $true 1;
    if ($true) {
        # nothing
    }
}
 
# 因为if, try_files 失效
location /if-try-files {
     try_files  /file  @fallback;
     set $true 1;
     if ($true) {
         # nothing
     }
}
 
# nginx 将会发出段错误信号(SIGSEGV)
location /crash {
    set $true 1;
    if ($true) {
        # fastcgi_pass here
        fastcgi_pass  127.0.0.1:9000;
    }
    if ($true) {
        # no handler here
    }
}
 
# alias with captures isn't correcly inherited into implicit nested location created by if
# alias with captures 不能正确的继承到由if创建的隐式嵌入的location 
location ~* ^/if-and-alias/(?.*) {
    alias /tmp/$file;
    set $true 1;
    if ($true) {
        # nothing
    }
}

 

下面针对每一个例子,我们先把配置片断完整列出来。然后,再给出目前它在 Nginx 中的 状态。最后,如果文字无法很好解释清楚,我们再从代码上对该例子进行分析。

3.2 例 1

# only second header will be present in respons # not really bug, just how it works 
location /only-one-if {  
  set $true 1;   
  if ($true) {  
    add_header X-First 1;  
  }   
  if ($true) {  
    add_header X-Second 2;  
  }   
  return 204; 
} 
  • 依然和原文描述一致。它属于 Nginx 的固有工作模式。

  • 原因:

    • add_header 指令是 ngx_http_headers_module 模块 (output filter module) 提供的指令,这个指令基本算是工作在 CONTENT 阶段

    • REWRITE 阶段的指令执行完后,请求才会进入下一个阶段。那两个 if 指令 的 condition 都是true,那么最后一个 if {} 会成为请求关联的 location 作用域。于是,进入 CONTENT 阶段后,第二个 if {} 中的 add_header 指 令才会生效

3.3 例 2

# request will be sent to backend without uri changed # to '/' due to if 
location /proxy-pass-uri {  
  proxy_pass http://127.0.0.1:8080/;   
  set $true 1;   
  if ($true) {  
    # nothing  
  } 
} 
  • Nginx BUG, 因为最新稳定版 1.8.0 已经修正了:ChangeLog

  • 大致原因就是因为往 if {} 创建的无名 location {} 命并配置时,漏了一个变量 plcf->location。

  • 另外,值得提起的一个点是:proxy_pass 是一个 action directive, 按照定义和其它嵌套location 的实现来看,这个指令不应该被子作用域继承,但是 if {} 当作子作用域的话,貌似是个例外,原因如下:

    • proxy_pass, fastcgi_pass 等指令提供了 location handler。一般情况下, Nginx 开始在FIND_CONFIG 阶段处理请求时,因为要为请求匹配新的 location {}, 在ngx_http_core_find_config_phase 函数里先将 r->content_handler 置 NULL,匹配成功后,如果匹配到的 location {} 提供了 location handler, 再次在ngx_http_update_location_config 函数中将 r->content_handler 赋与 新值:

      /* ngx_http_proxy_pass */ 
      clcf = ngx_http_conf_get_module_loc_conf(cf, ngx_http_core_module); 
      clcf->handler = ngx_http_proxy_handler;  
      
      /* ngx_http_core_find_config_phase */ 
      r->content_handler = NULL;  
      
      /* ngx_http_update_location_config */ 
      if (clcf->handler) {  
        r->content_handler = clcf->handler; 
      } 
    • 由上一篇的分析我们能看到,如果 if 指令后的 conditon 成立,在命令函数ngx_http_script_if_code 中只调用了 ngx_http_update_location_config 函数 而并没有让请求重新进入 FIND_CONFIG 阶段。这样一来,r->content_handler 的值就得以保留。

    • 这也算是 if 的实现带来的 “副” 作用吧;

3.4 例 3

# try_files won't work due to if 
location /if-try-files {  
  try_files /file @fallback;   
  set $true 1;   
  if ($true) {  
    # nothing  
  } 
} 
  • Nginx 固有逻辑

  • 原因:首先,try_files 不会被子作用域继承。其次,try_files 工作于 Nginx 内部定义的TRY_FILES 阶段,并且此阶段优先级低于 REWRITE。所以,当 if 指令的 condition 成立时,请求和 if 创建的无名作用域关联,而此无名作用 域又不会继承 try_files 相关配置

3.5 例 4

# nginx will SIGSEGV 
location /crash {  
  set $true 1;   
  if ($true) {  
    fastcgi_pass 127.0.0.1:9000;  
  }   
  if ($true) {  
    # no handler here  
  } 
} 
  • Nginx BUG,目前稳定版本 1.2.0 已修正:Changelog
  • 原因:参见 ChangeLog。

3.6 例 5

# alias with captures isn't correctly inherited into implicit nested # location created by if 
location ~* ^/if-and-alias/(?<file>.*) {  
  alias /tmp/$file;   
  set $true 1;   
  if ($true) {  
    # nothing  
  } 
} 
  • Nginx BUG, 目前在稳定版 1.8.0 已修正: Changelog
  • 原因:参见 ChangeLog。

3.7 小结

"if is evil" 一文中出现的例子要么已经被修正了,要么就是确实不能那么用。但是, 原文不是所有if 带来的问题大全,兴许某一天,你自己就会碰见。希望能通过这篇的讲解,将来某一天碰到莫名的问题时,你能自己分析原因,说不定还能找到 Nginx 的 BUG 呢。

4 如何避免

既然 if 如此之坑,最好的办法就是乖乖禁用 if,但拗不过诸位喜欢折腾的灵魂,特殊场景还是可以用的。下面列举最常见的避坑方案。

4.1 官方认可的两种用法

再怎么说,if 是为 rewrite 服务的,在正确的场合做正确的事,没毛病。

#The only 100% safe things which may be done inside if in a location context are:

return ...;
rewrite ... last;

4.2 proxy_pass 中及时 break

当处理各种跳转逻辑时,假设你不期望因为多传了个等于 “hi” 的参数 parma2,就导致 param1 ~ "hello" 的跳转失败,那么应该在合适的地方增加 break。

location /proxy-pass-uri {
    set $true 1;

    if ($arg_param1 ~ "hello") {
      proxy_pass http://127.0.0.1:8080/;
      # 这里的 break 将阻止下面 if 的执行
      break;
    }

    if ($arg_param2 ~ "hi") {
      # anything
    }

    proxy_pass http://127.0.0.1:8082/;
}

此外,也可以通过调整 if 的顺序来规避风险,但是容错率比 break 低了很多

又例如:

server {
listen 127.0.0.1:80;
        set $test  A; 
        set $testB B;
        location / {
           if ($test ~* "A") { 
                 proxy_pass http://www.so.com; 
                 break; 
           } 
           if ($testB ~* "B") { 
                 proxy_pass http://www.sogou.com; 
                 #break; 
           } 
       }
}

希望满足某个条件的时候,走某个proxy_pass。但是如果多个if都满足,比如上例中的情况,在没有break的时候,会执行下面的if;

为了第一个匹配上之后就执行proxy_pass,可以加上break

4.3 或者条件互斥

判断参数进行不同的proxy_pass:

rewrite只能通过url路径进行匹配,不能进行参数匹配,所以如果要实现参数的判断需要用$arg_parameter。

location / {
        root    html;
        index   index.html index.htm index.php;
        proxy_redirect      off;
        proxy_set_header    X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header    X-Real-IP $remote_addr;
        proxy_set_header    Host $http_host;
        proxy_http_version  1.1;
        proxy_set_header    Connection "";
 
        if ( $query_string ~* "usg=0" ) {
            proxy_pass      http://local_workera;
        }
 
        if ( $query_string ~* "usg=1" ) {
            proxy_pass      http://local_workerb;
        }
 
        if ( $arg_uid ~* "(.*[AB]$)" ) {
            proxy_pass      http://local_workerf;
        }
 
        proxy_pass    http://local_workera;
}
1)请求路径中的usg=0和usg=1这两个参数是确定的,所以使用了$query_string进行正则匹配即可;($query_string的值是请求中所有参数)

2)接下来,我们想对uid的值如果是以A、B结尾的请求,转向local_workerf处理,这时候就无法用$query_string进行正则匹配了;(因为对于/?uid=1A&t=1&usg=1和/?uid=123&t=A&usg=0 不太好匹配)这时,只能用$arg_uid进行正则匹配了。

3)由于usg=0和usg=2这两个参数是互斥的,所以根据上面location中if指令的逻辑,不用break也可以正确处理,且放到最上面。对于uid的匹配,由于会和usg进行冲突,所以只能放到最下面或者加break,

即:

location / {
        root    html;
        index   index.html index.htm index.php;
        proxy_redirect      off;
        proxy_set_header    X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header    X-Real-IP $remote_addr;
        proxy_set_header    Host $http_host;
        proxy_http_version  1.1;
        proxy_set_header    Connection "";
        
        if ( $arg_uid ~* "(.*[AB]$)" ) {
            proxy_pass      http://local_workerf;
            break;
        }
        # 下面两个条件互斥   不用break
        if ( $query_string ~* "usg=0" ) {
            proxy_pass      http://local_workera;
        }
 
        if ( $query_string ~* "usg=1" ) {
            proxy_pass      http://local_workerb;
        }
 
        proxy_pass    http://local_workera;
}
 

4.4 server_name指令与if

劣:

server {
    server_name example.com *.example.com;
        if ($host ~* ^www\.(.+)) {
            set $raw_domain $1;
            rewrite ^/(.*)$ $raw_domain/$1 permanent;
        }
        # [...]
    }
}

这里实际上有三个问题。首先看看if,也是我们现在要关注的,为何是劣的?你读过这篇文章 If是魔鬼 吗?当nginx接收到一个请求不管子域名是什么,即使是www.example.com或example.com,if指令也是每次都要调用,因为NGINX需要每次对请求头Host做处理,这特别没有效率,你要千万注意避免它。可以替换为两个server区块,如下面的列子:

优:

server {
    server_name www.example.com;
    return 301 $scheme://example.com$request_uri;
}
server {
    server_name example.com;
    # [...]
}

这种配置也可以使配置更好阅读,这种方法减少了NGINX的处理需求。我们去除了谬误的if,同时我们使用了$schema避免了硬编码URI,如http或https。

4.5 检查文件是否存在(if)

使用if来判断一个文件是否存在太可怕了。也就是说如果你有最新版的NGINx,使用tryfiles指令将更加容易。

劣:

server {
    root /var/www/example.com;
    location / {
        if (!-f $request_filename) {
            break;
        }
    }
}
优:
server {
    root /var/www/example.com;
    location / {
        try_files $uri $uri/ /index.html;
    }
}

我们只是修改了判断文件是否存在但又不使用if指令,使用tryfiles表示可以测试一系列文件,如果$uri不存在,再试$uri/,如果还不存在,就使用默认位置(index.html)。

在这个列子,如果$uri存在就使用它,如果不存在,再判断$uri/目录是否存在,如果不存在就使用index.html,必须要保证最后一个检测的文件存在。很简单吧!下面是另一个例子去掉if指令。

4.6 利用变量来判断

nginx虽然有if,但是却不支持else,如果想要构造else语法,可以使用下面的这个“小诀窍”:

server {
    server_name *.maqian.io;
    listen 80;
    location / {
        set $is_matched 0;
        if ($host = a.maqian.io) {
            proxy_pass https://127.0.0.1:1001/;
            set $is_matched 1;
        }

        if ($host = b.maqian.io) {
            proxy_pass https://127.0.0.1:1002/;
            set $is_matched 1;
        }

        # 没有匹配到,跳转到默认页面
        if ($is_matched = 0) {
            proxy_pass https://127.0.0.1;
        }
        # xxx
        # xxx
        # xxx
    }
}

4.7 多条件判断

4.7.1 例子1

要实现的语句:

if ($arg_unitid = 42012 && $uri ~/thumb/){
 echo "www.ttlsa.com";
}

可以这么来实现,如下所示:

set $flag 0;
if ($uri ~ ^/thumb/[0-9]+_160.jpg$){
 set $flag "${flag}1";
}
if ($arg_unitid = 42012){
 set $flag "${flag}1";
}
if ($flag = "011"){
 echo "www.ttlsa.com";
}

4.7.2 例子2

实现禁止ip访问,禁止用post访问非php的文件

set $allowss true;
set $allowphp '';

if ($request_method ~ ^(POST)$) {
    set $allowphp p;
}

if ( $request_filename !~ \.php$) {
    set $allowphp "${allowphp}c";
}

if ( $allowphp = pc) {
    set $allowss false;
}

if ($http_x_forwarded_for ~ " ?222\.186\.34\.41$") {
    set $allowss false;
}

if ($http_x_forwarded_for ~ " ?180\.97\.106\.37$") {
    set $allowss false;
}

if ($allowss = false) {
    return 403;
}

4.7.3 例子3

如果需要分支`if`语句,则很明显,仅释放带有`last`标志的重写语句,如下所示,并在下一个块中实现其他工作。
 
location /proxy {
    ...
    if ($agent = a) {
        rewrite ^ /xxx last;
    }
    if ($agent = b) {
        rewrite ^ /yyy last;
    }
    if ($agent = c) {
        rewrite ^ /zzz last;
    }
    rewrite ^ /other;
}

 

5 使用 lua

https://github.com/openresty/lua-nginx-module

       具体来说,通过安装 lua 和 lua-nginx-module( 或 OpenResty ) 来增强 nginx 的逻辑处理,可以称为目前最流行有效的处理方案。lua 扩展带来的好处远不止 if 语句的避坑,例如带来方便地读取 POST 请求体的内容等便利操作。网上资料齐全,这里不多赘述。

6 使用 njs

http://nginx.org/en/docs/njs/index.html

       nginScript (njs) 是 nginx 在 2015 发布的 javascript 配置方案。类似于 lua-nginx-module,它也需要安装相关依赖模块。但有官方做后盾的 njs,比起 lua 要更为轻量,不需要完整的语言运行环境。并且针对 nginx 环境进行设计,理论上有更高的优化空间。

 

7 其他《nginx的location if是如何工作的》

出处:

http://agentzh.blogspot.com/2011/03/how-nginx-location-if-works.html

nginx的location if是如何工作的

       Nginx的if指令在实际运用中有一些诡异的地方。人们常常会因为对它的行为缺乏足够的知识而可能误用它。在这篇博客中,我将分析一些例子,这样人们可能会受些启发而正确使用它。

       简而言之,一旦if语句条件匹配,Nginx的if块(block)实际上创建了一个(内嵌的)location块(block),只有内部的location块(例如if块)的content handler会被执行。

7.1 示例1

  1. location /proxy {  
  2.       set $a 32;  
  3.       if ($a = 32) {  
  4.           set $a 56;  
  5.       }  
  6.       set $a 76;  
  7.       proxy_pass http://127.0.0.1:$server_port/$a;  
  8.   }  
  9.   location ~ /(\d+) {  
  10.       echo $1;  
  11.   }  

 访问/proxy会显示76

因为会像下列步骤执行:
1.Nginx会按照在配置文件的出现顺序执行所有的rewrite阶段的指令,比如下面顺序执行:

  1. set $a 32;  
  2. if ($a = 32) {  
  3.       set $a 56;  
  4. }  
  5. set $a 76;  

$a的最后为76。


2.因为在上个阶段$a = 32条件匹配,Nginx会进入if内部location块。
3.这个if内部location块没有任何content handler, 在外部域(outer scope)
nginx_proxy继承了content handler(参考src/http/modules/ngx_http_proxy_module.c:2025)。
4.proxy指定的配置也间接继承这个内部if块(详见src/http/modules/ngx_http_proxy_module.c:2015)。
5.请求终止(控制流永远不会越过这个if块)。
就是说,在此例子中在外部域的proxy_pass指令永远不会执行。实际上只有这个内部的if块为你服务。

简而言之:if block模块没有content handler,所以if block模块继承了外部模块/proxy 的`proxy_pass`,最后再if模块里执行了这个`proxy_pass`,而不是在/proxy模块中执行的`proxy_pass`。

 

让我们再来看看当我们复写了内部if块的content handler会发生什么。

7.2 示例2

  1. location /proxy {  
  2.       set $a 32;  
  3.       if ($a = 32) {  
  4.           set $a 56;  
  5.           echo "a = $a";  
  6.       }  
  7.       set $a 76;  
  8.       proxy_pass http://127.0.0.1:$server_port/$a;  
  9.   }  
  10.   location ~ /(\d+) {  
  11.       echo $1;  
  12.   }  

访问/proxy会得到a = 76
看起来不符合常理?那让我们来看看这次发生了什么:
1.Nginx会按照配置文件出现的顺序来执行所有的rewrite阶段的指令,比如:

  1. set $a 32;  
  2.       if ($a = 32) {  
  3.           set $a 56;  
  4.       }  
  5.   set $a 76;  

2.因为在上个阶段$a = 32条件匹配则Nginx跳入内部if块。
3.这个内部块这次有了一个content handler,被echo指令指定,这样$a的值(76)会被发送给客户端
4.请求终止,请求终止(控制流永远不会越过这个if块),如同第一个示例。

我们让示例2变得按照我们的意愿来工作:

7.3 示例3

  1. location /proxy {  
  2.       set $a 32;  
  3.       if ($a = 32) {  
  4.           set $a 56;  
  5.           break;  
  6.           echo "a = $a";  #echo是content handler,还是会被执行
  7.       }  
  8.       set $a 76;  
  9.       proxy_pass http://127.0.0.1:$server_port/$a;  
  10.   }  
  11.   location ~ /(\d+) {  
  12.       echo $1;  
  13.   }  

 这次,我们仅仅在if块里加了一个break指令,这回让nginx中止执行剩下的ngx_rewrite指令,所以我们会得到a = 56。

所以这一次,nginx会这样工作:
1.Nginx会按照配置文件出现的顺序来执行所有的rewrite阶段的指令,比如:

  1. set $a 32;  
  2. if ($a = 32) {  
  3.       set $a 56;  
  4.       break;  
  5. }  

$a最后为56。
2.因为在上个阶段$a = 32条件匹配则Nginx跳入内部if块。
3.这个内部块这次有了一个content handler,被echo指令指定,这样$a的值(56)会被发送给客户端。
4.请求终止,请求终止(控制流永远不会越过这个if块),如同第一个示例。

 

OK,你在这里看到ngx_proxy模块在嵌入的多个location间的配置的继承扮演了关键的角色,这能让你按的心愿来工作。但是其他模块(比如echo,在作者之间的邮件提及)可能不会继承内嵌的location的content handler(事实上,大多是content handler模块,包括upstream都不支持继承)。


在其它一些情况下,关于if块的配置文件的继承还有一些副作用需要注意,考虑下面的例子:

7.4 示例4

  1. location /proxy {  
  2.       set $a 32;  
  3.       if ($a = 32) {  
  4.           return 404;  
  5.       }  
  6.       set $a 76;  
  7.       proxy_pass http://127.0.0.1:$server_port/$a;  
  8.       more_set_headers "X-Foo: $a";  
  9.   }  
  10.   
  11.   location ~ /(\d+) {  
  12.       echo $1;  
  13.   }  

 
在这里,ngx_header_more模块的more_set_headers指令也会继承if块隐式创建的location。所以你会得到:

  1. $ curl localhost/proxy  
  2.   HTTP/1.1 404 Not Found  
  3.   Server: nginx/0.8.54 (without pool)  
  4.   Date: Mon, 14 Feb 2011 05:24:00 GMT  
  5.   Content-Type: text/html  
  6.   Content-Length: 184  
  7.   Connection: keep-alive  
  8.   X-Foo: 32  

 这可能是你想要的,或者是你不想要的:)


顺便说说,如果在这个例子中换成add_header指令,将不会发送X-Foo头,这不代表没有发生指令的继承,而是add_header的头过滤器(header filter)会跳过404响应。

 

7.5 示例5

 
In Case 3, the `return` syntax was used instead of `break`.
 
location /proxy {
    set $a 10; # (1)
    if ($a = 10) {# (2)
        set $a 20; # (3)
        return 200'a = $a'; # (4)
    }
    set $a 30; # (5)
    proxy_pass http://127.0.0.1:$server_port/$a; # (6)
}
 
When a request comes in to /proxy, the request process is terminated at (1) -> (2) -> (3) -> (4).
Probably pretty much the same as intended, and you see `a = 20` in the response.
 
Since the process ends immediately on that line, it can be understood in the same context as `if` in a general language.
So, it seems that only `return` is safe in the wiki.
 
The `last` flag is equally safe,
If line (4) is the same as  `rewrite ^ /foo last;` , processing ends immediately after `rewrite` in the line.
 
 

7.6 示例6

 
In Case 4, the `add_header` module was added.
 
location /proxy {
    set $a 10; # (1)
    if ($a = 10) {# (2)
        return 200'a = $a'; # (3)
    }
    add_header x-foo true; # (4)
}
 
When a request comes in to /proxy, the process is terminated at (1) -> (2) -> (3),
The value of `add_header` is inherited, so `x-foo: true` is included in the response.
 
 

7.7 示例7

 
In Case 5, the `add_header` module is additionally defined in the `if` block.
 
location /proxy {
    set $a 10; # (1)
    if ($a = 10) {# (2)
        return 200'a = $a'; # (3)
        add_header x-bar true; # (4)
    }
    add_header x-foo true; # (5)
}
 
When a request comes in to /proxy, the process is terminated at (1) -> (2) -> (3),
According to the `add_header` module in (4), the response includes `x-bar: true` header.
 
Line (5) is not executed. Note that no additional headers are defined.
 
 

7.8 示例8

 
Added a few `rewrite` statements in the `if` block.
 
location /proxy {
    set $a 10; # (1)
    rewrite ^ /aaa; # (2)
    if ($a = 10) {# (3)
        rewrite ^ /bbb; # (4)
    }
    if ($a = 10) {# (5)
        # none
    }
    rewrite ^ /ccc; # (6)
}
 
When a request comes in to /proxy, it is executed in the order of (1) -> (2) -> (3) -> (4) -> (5) -> (6), and finally rewritten as `/ccc`. .
 
By using the `break` or `last` flag in each `rewrite` statement, the behavior can be changed as desired.
For example, if you say `rewrite ^ /bbb last;` on line (4), it will rewrite with `/bbb` and terminate the process.
 
 
 

8 总结

 
      已经上面的说明,现在大家了解了它的工作原理。

      但是,处理方法似乎根据处理程序的操作或模块的继承而有所不同,因此无法仅通过查看规则来确定。(例如`add_header`和`more_set_header`),如Wiki所述,最好尽可能避免使用它。

 
 

参考:

https://www.nginx.com/resources/wiki/start/topics/depth/ifisevil/#examples

https://toutiao.io/posts/mi41q/preview

http://claude-ray.com/2019/01/09/nginx-ifisevil/

https://www.icode9.com/content-3-58703.html

https://wiki.jikexueyuan.com/project/openresty/ngx/pitfalls_and_common_mistakes.html

https://www.iteye.com/blog/damacheng009-1583879

https://ohgyun.com/607

  • 3
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值