NginxDirectiveExecOrderTutorialCn03

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

如前文所述，除非像 ngx_set_misc 模块那样使用特殊技术，其他模块的配置指令即使是在  阶段运行，也不能和 ngx_rewrite 模块的指令混合使用. 不妨来看几个这样的例子.

第三方模块 ngx_headers_more 提供了一系列配置指令，用于操纵当前请求的请求头和响应头. 其中有一条名叫 more_set_input_headers 的指令可以在  阶段改写指定的请求头（或者在请求头不存在时自动创建）. 这条指令总是运行在  阶段的末尾，该指令的文档中有这么一行标记：

phase: rewrite tail

其中的  的意思就是   阶段的末尾.

既然运行在  阶段的末尾，那么也就总是会运行在   模块的指令之后，即使我们在配置文件中把它写在前面，例如：

? location /test { ?    set $value dog; ?    more_set_input_headers "X-Species: $value"; ?    set $value cat; ?   ?     echo "X-Species: $http_x_species"; ? }

这个例子用到的 $http_XXX 内建变量在读取时会返回当前请求中名为  的请求头，我们在 Nginx 变量漫谈（二） 中曾经简单提过它. 需要注意的是，$http_XXX 变量在匹配请求头时会自动对请求头的名字进行归一化，即将名字的大写字母转换为小写字母，同时把间隔符（ ）替换为下划线（ ），所以变量名  才得以成功匹配 more_set_input_headers 语句中设置的请求头.

此例书写的指令顺序会误导我们认为  接口输出的   头的值是  ，然而实际的结果却并非如此：

$ curl 'http://localhost:8080/test' X-Species: cat

显然，写在 more_set_input_headers 指令之后的  语句却先执行了.

上面这个例子证明了即使运行在同一个请求处理阶段，分属不同模块的配置指令也可能会分开独立运行（除非像 ngx_set_misc 等模块那样针对 ngx_rewrite 模块提供特殊支持）. 换句话说，在单个请求处理阶段内部，一般也会以 Nginx 模块为单位进一步地划分出内部子阶段.

第三方模块 ngx_lua 提供的 rewrite_by_lua 配置指令也和 more_set_input_headers 一样运行在  阶段的末尾. 我们来验证一下：

? location /test { ?    set $a 1; ?    rewrite_by_lua "ngx.var.a = ngx.var.a + 1"; ?    set $a 56; ?   ?     echo $a; ? }

这里我们在 rewrite_by_lua 语句内联的 Lua 代码中对 Nginx 变量  进行了自增计算. 从该例的指令书写顺序上看，我们或许会期望输出是 ，可是因为 rewrite_by_lua 会在所有的 set 语句之后执行，所以结果是  ：

$ curl 'http://localhost:8080/test' 57

显然，rewrite_by_lua 指令的行为不同于我们前面在 （二） 中介绍过的 set_by_lua 指令.

有的读者可能要问，既然 more_set_input_headers 和 rewrite_by_lua 指令都运行在  阶段的末尾，那么它们之间的先后顺序又是怎样的呢？答案是：不一定. 我们应当避免写出依赖它们二者间顺序的配置.

Nginx 的  阶段是一个比较早的请求处理阶段，这个阶段的配置指令一般用来对当前请求进行各种修改（比如对 URI 和 URL 参数进行改写），或者创建并初始化一系列后续处理阶段可能需要的 Nginx 变量. 当然，也不能阻止一些用户在  阶段做一系列更复杂的事情，比如读取请求体，或者访问数据库等远方服务，毕竟有 rewrite_by_lua 这样的指令可以嵌入任意复杂的 Lua 代码.

在  阶段之后，有一个名叫   的请求处理阶段. Nginx 变量漫谈（五） 中介绍过的第三方模块 ngx_auth_request 的指令就运行在  阶段. 在  阶段运行的配置指令多是执行访问控制性质的任务，比如检查用户的访问权限，检查用户的来源 IP 地址是否合法，诸如此类.

例如，标准模块 ngx_access 提供的 allow 和 deny 配置指令可用于控制哪些 IP 地址可以访问，哪些不可以：

location /hello { allow 127.0.0.1; deny all;

echo "hello world"; }

这个  接口被配置为只允许从本机（IP 地址为保留的  ）访问，而从其他 IP 地址访问都会被拒（返回   错误页）. ngx_access 模块自己的多条配置指令之间是按顺序执行的，直到遇到第一条满足条件的指令就不再执行后续的 allow 和 deny 指令. 如果首先匹配的指令是 allow，则会继续执行后续其他模块的指令或者跳到后续的处理阶段；而如果首先满足的是 deny 则会立即中止当前整个请求的处理，并立即返回  错误页. 所以看上面这个例子，如果是从本地访问的，则首先匹配  这一条语句，于是 Nginx 就继续往下执行其他模块的指令以及后续的处理阶段；而如果是从其他机器访问，则首先匹配的则是   这一条语句，即拒绝所有地址，它会导致   错误页立即返回给客户端.

我们来实测一下. 从本机访问这个接口可以得到

$ curl 'http://localhost:8080/hello' hello world

而从另一台机器访问这台机器（假设运行 Nginx 的机器地址是 ）提供的接口时则得到

$ curl 'http://192.168.1.101:8080/hello' 403 Forbidden 403 Forbidden nginx

值得一提的是，ngx_access 模块还支持所谓的“CIDR 记法”来表示一个网段，例如  则表示路由前缀是  （或者说子网掩码是  ）的网段.

因为 ngx_access 模块的指令运行在  阶段，而   阶段又处于   阶段之后，所以前面我们见到的所有那些在   阶段运行的配置指令，都总是在 allow 和 deny 之前执行，而无论它们在配置文件中的书写顺序是怎样的. 所以，为了避免阅读配置时的混乱，我们应该总是让指令的书写顺序和它们的实际执行顺序保持一致.