NginxDirectiveExecOrderTutorialCn10

= Nginx 配置指令的执行顺序（十） =

运行在  阶段之后的是所谓的   阶段. 该阶段在  阶段之前执行，故名.

标准模块 ngx_limit_req 和 ngx_limit_zone 就运行在此阶段，前者可以控制请求的访问频度，而后者可以限制访问的并发度. 这里我们仅仅和它们打个照面，后面还会有机会专门接触到这两个模块.

前面反复提到的标准模块 ngx_realip 其实也在这个阶段注册了处理程序. 有些读者可能会问：“这是为什么呢？它不是已经在  阶段注册处理程序了吗？”我们不妨通过下面这个例子来揭晓答案：

server { listen 8080;

location /test { set_real_ip_from 127.0.0.1; real_ip_header X-Real-IP;

echo "from: $remote_addr"; }   }

与先看前到的例子相比，此例最重要的区别在于把 ngx_realip 的配置指令放在了  配置块中. 前面我们介绍过，Nginx 匹配  的动作发生在   阶段，而   阶段远远晚于   阶段执行，所以在   阶段，当前请求还没有和任何   相关联. 在这个例子中，因为 ngx_realip 的配置指令都写在了  配置块中，所以在   阶段，ngx_realip 模块的处理程序没有看到任何可用的配置信息，便不会执行来源地址的改写工作了.

为了解决这个难题，ngx_realip 模块便又特意在  阶段注册了处理程序，这样它才有机会运行   块中的配置指令. 正是因为这个缘故，上面这个例子的运行结果才符合直觉预期：

$ curl -H 'X-Real-IP: 1.2.3.4' localhost:8080/test from: 1.2.3.4

不幸的是，ngx_realip 模块的这个解决方案还是存在漏洞的，比如下面这个例子：

server { listen 8080;

location /test { set_real_ip_from 127.0.0.1; real_ip_header X-Real-IP;

set $addr $remote_addr; echo "from: $addr"; }   }

这里，我们在  阶段将 $remote_addr 的值保存到了用户变量   中，然后再输出. 因为  阶段先于   阶段执行，所以当 ngx_realip 模块尚未在   阶段改写来源地址时，最初的来源地址就已经在   阶段被读取了. 上例的实际请求结果证明了我们的结论：

$ curl -H 'X-Real-IP: 1.2.3.4' localhost:8080/test from: 127.0.0.1

输出的地址确实是未经改写过的. Nginx 的“调试日志”可以进一步确认这一点：

$ grep -E 'http script (var|set)|realip' logs/error.log [debug] 32488#0: *1 http script var: "127.0.0.1" [debug] 32488#0: *1 http script set $addr [debug] 32488#0: *1 realip: "1.2.3.4" [debug] 32488#0: *1 realip: 0100007F FFFFFFFF 0100007F [debug] 32488#0: *1 http script var: "127.0.0.1"

其中第一行调试信息

[debug] 32488#0: *1 http script var: "127.0.0.1"

是 set 语句读取 $remote_addr 变量时产生的. 信息中的字符串  便是 $remote_addr 当时读出来的值.

而第二行调试信息

[debug] 32488#0: *1 http script set $addr

则显示我们对变量  进行了赋值操作.

后面两行信息

[debug] 32488#0: *1 realip: "1.2.3.4" [debug] 32488#0: *1 realip: 0100007F FFFFFFFF 0100007F

是 ngx_realip 模块在  阶段改写当前请求的来源地址. 我们看到，改写后的新地址确实是期望的. 但很明显这个操作发生在  变量赋值之后，所以已经太迟了.

而最后一行信息

[debug] 32488#0: *1 http script var: "127.0.0.1"

则是 echo 配置指令在输出时读取变量  时产生的，我们看到它的值是改写前的来源地址.

看到这里，有的读者可能会问：“如果 ngx_realip 模块不在  阶段注册处理程序，而在   阶段注册，那么上例不就可以工作了？”答案是：不一定. 因为 ngx_rewrite 模块的处理程序也同样注册在  阶段，而前面我们在 （二） 中特别提到，在这种情况下，不同模块之间的执行顺序一般是不确定的，所以 ngx_realip 的处理程序可能仍然在 set 语句之后执行.

一个建议是：尽量在  配置块中配置 ngx_realip 这样的模块，以避免上面介绍的这种棘手的例外情况.

运行在  阶段之后的则是我们的另一个老朋友，  阶段. 前面我们已经知道了，标准模块 ngx_access、第三方模块 ngx_auth_request 以及第三方模块 ngx_lua 的 access_by_lua 指令就运行在这个阶段.

阶段之后便是  阶段. 从这个阶段的名字，我们也能一眼看出它是紧跟在  阶段后面执行的. 这个阶段也和  阶段类似，并不支持 Nginx 模块注册处理程序，而是由 Nginx 核心自己完成一些处理工作. 阶段主要用于配合  阶段实现标准 ngx_http_core 模块提供的配置指令 satisfy 的功能.

对于多个 Nginx 模块注册在  阶段的处理程序，satisfy 配置指令可以用于控制它们彼此之间的协作方式. 比如模块 A 和 B 都在  阶段注册了与访问控制相关的处理程序，那就有两种协作方式，一是模块 A 和模块 B 都得通过验证才算通过，二是模块 A 和模块 B 只要其中任一个通过验证就算通过. 第一种协作方式称为  方式（或者说“与关系”），第二种方式则被称为   方式（或者说“或关系”）. 默认情况下，Nginx 使用的是  方式. 下面是一个例子：

location /test { satisfy all;

deny all; access_by_lua 'ngx.exit(ngx.OK)';

echo something important; }

这里，我们在  接口中同时配置了 ngx_access 模块和 ngx_lua 模块，这样   阶段就由这两个模块一起来做检验工作. 其中，语句  会让 ngx_access 模块的处理程序总是拒绝当前请求，而语句   则总是允许访问. 当我们通过 satisfy 指令配置了  方式时，就需要   阶段的所有模块都通过验证，但不幸的是，这里 ngx_access 模块总是会拒绝访问，所以整个请求就会被拒：

$ curl localhost:8080/test 403 Forbidden 403 Forbidden nginx

细心的读者会在 Nginx 错误日志文件中看到类似下面这一行的出错信息：

[error] 6549\#0: *1 access forbidden by rule

然而，如果我们把上例中的  语句更改为  ，

location /test { satisfy any;

deny all; access_by_lua 'ngx.exit(ngx.OK)';

echo something important; }

结果则会完全不同：

$ curl localhost:8080/test something important

即请求反而最终通过了验证. 这是因为在  方式下，  阶段只要有一个模块通过了验证，就会认为请求整体通过了验证，而在上例中，ngx_lua 模块的 access_by_lua 语句总是会通过验证的.

在配置了  的情况下，只有当   阶段的所有模块的处理程序都拒绝访问时，整个请求才会被拒，例如：

location /test { satisfy any;

deny all; access_by_lua 'ngx.exit(ngx.HTTP_FORBIDDEN)';

echo something important; }

此时访问  接口才会得到   错误页. 这里， 阶段参与了   阶段各模块处理程序的“或关系”的实现.

值得一提的是，上面这几个的例子需要 ngx_lua 0.5.0rc19 或以上版本；之前的版本是不能和  配置语句一起工作的.