rewrite阶段
rewrite阶段是一个比较早的请求处理阶段,这个阶段的配置指令一般用来对当前请求进行各种修改(比如对URI和URL参数进行改写),或者创建并初始化一系列后续处理阶段可能需要的Nginx变量。当然,也不能阻止一些用户在rewrite阶段做一系列更复杂的事情,比如读取请求体,或者访问数据库等远方服务,毕竟有rewrite_by_lua这样的指令可以嵌入任意复杂的 Lua 代码。
一、示例1
location /test {
set $a 32;
echo $a;
set $a 56;
echo $a;
}
从这个例子的本意来看,我们期望的输出是一行32和一行56,因为我们第一次用echo配置指令输出了$a变量的值以后,又紧接着使用set配置指令修改了$a. 然而不幸的是,事实并非如此:
$ curl 'http://localhost:8080/test
56
56
首先需要知道Nginx处理每一个用户请求时,都是按照若干个不同阶段(phase)依次处理的。
Nginx 的请求处理阶段共有 11 个之多,我们先介绍其中 3 个比较常见的。按照它们执行时的先后顺序,依次是rewrite阶段、access阶段以及content阶段。
所有 Nginx 模块提供的配置指令一般只会注册并运行在其中的某一个处理阶段。比如上例中的 set 指令就是在rewrite阶段运行的,而echo指令就只会在content 阶段运行。前面我们已经知道,在单个请求的处理过程中,rewrite阶段总是在content阶段之前执行,因此属于rewrite阶段的配置指令也总是会无条件地在content阶段的配置指令之前执行。于是在同一个 location配置块中,set指令总是会在echo指令之前执行,即使我们在配置文件中有意把set语句写在echo语句的后面。
回到刚才那个例子,实际的执行顺序应当是
set $a 32;
set $a 56;
echo $a;
echo $a;
即先在rewrite阶段执行完这里的两条set赋值语句,然后再在后面的content阶段依次执行那两条echo语句。分属两个不同处理阶段的配置指令之间是不能穿插着运行的。
想要达到预期的效果,配置如下:
location /test {
set $a 32;
set $saved_a $a;
set $a 56;
echo $saved_a;
echo $a;
}
输出结果:
$ curl 'http://localhost:8080/test'
32
56
这里通过引入新的用户变量$saved_a,在改写$a之前及时保存了$a的初始值。而对于多条set指令而言,它们之间的执行顺序是由ngx_rewrite模块来保证与书写顺序相一致的。同理,ngx_echo模块自身也会保证它的多条echo指令之间的执行顺序。
二、示例2
当set指令用在location配置块中时,都是在当前请求的rewrite阶段运行的。事实上,在此上下文中,ngx_rewrite模块中的几乎全部指令,都运行在rewrite阶段,包括rewrite指令。不过,值得一提的是,当这些指令使用在server配置块中时,则会运行在一个我们尚未提及的更早的处理阶段,server-rewrite阶段。
location /test {
set $a "hello%20world";
set_unescape_uri $b $a;
set $c "$b!";
echo $c;
}
访问这个接口可以得到:
$ curl 'http://localhost:8080/test'
hello world!
"hello%20world" 在这里被成功解码为 "hello world"
location /test {
set $a 32;
set $b 56;
set_by_lua $c "return ngx.var.a + ngx.var.b";
set $equation "$a + $b = $c";
echo $equation;
}
这里我们先将$a和$b变量分别初始化为32和56,然后利用set_by_lua指令内联一行我们自己指定的Lua代码,计算出Nginx变量$a和$b的“代数和”(sum),并赋给变量$c,接着利用“变量插值”功能,把变量$a、$b和$c的值拼接成一个字符串形式的等式,赋予变量$equation,最后再用echo指令输出$equation的值。
这个例子值得注意的地方是:首先,我们在Lua代码中是通过ngx.var.VARIABLE接口来读取Nginx变量$VARIABLE的;其次,因为Nginx变量的值只有字符串这一种类型,所以在Lua代码里读取ngx.var.a和ngx.var.b时得到的其实都是Lua字符串类型的值"32"和"56";接着,我们对两个字符串作加法运算会触发Lua对加数进行自动类型转换(Lua会把两个加数先转换为数值类型再求和);然后,我们在Lua代码中把最终结果通过return语句返回给外面的Nginx变量$c;最后,ngx_lua模块在给$c实际赋值之前,也会把return语句返回的数值类型的结果,也就是Lua加法计算得出的“和”,自动转换为字符串(这同样是因为Nginx变量的值只能是字符串)。
运行结果:
$ curl 'http://localhost:8080/test'
32 + 56 = 88
这些“常规模块”的指令虽然也运行在rewrite阶段,但其配置指令和ngx_rewrite模块(以及同一阶段内的其他模块)都是分开独立执行的。在运行时,不同模块的配置指令集之间的先后顺序一般是不确定的(严格来说,一般是由模块的加载顺序决定的,但也有例外的情况)。比如A和B两个模块都在rewrite阶段运行指令,于是要么是A模块的所有指令全部执行完再执行B模块的那些指令,要么就是反过来,把B的指令全部执行完,再去运行A的指令。除非模块的文档中有明确的交待,否则用户一般不应编写依赖于此种不确定顺序的配置。
三、示例3
第三方模块ngx_headers_more提供了一系列配置指令,用于操纵当前请求的请求头和响应头。其中有一条名叫more_set_input_headers的指令可以在rewrite阶段改写指定的请求头(或者在请求头不存在时自动创建)。这条指令总是运行在rewrite阶段的末尾,该指令的文档中有这么一行标记:
phase: rewrite tail
既然运行在rewrite阶段的末尾,那么也就总是会运行在ngx_rewrite模块的指令之后,即使我们在配置文件中把它写在前面,例如:
location /test {
set $value dog;
more_set_input_headers "X-Species: $value";
set $value cat;
echo "X-Species: $http_x_species";
}
这个例子用到的 $http_XXX 内建变量在读取时会返回当前请求中名为 XXX 的请求头。需要注意的是,$http_XXX变量在匹配请求头时会自动对请求头的名字进行归一化,即将名字的大写字母转换为小写字母,同时把间隔符(-)替换为下划线(_),所以变量名$http_x_species 才得以成功匹配more_set_input_headers语句中设置的请求头X-Species.
此例书写的指令顺序会误导我们认为 /test 接口输出的 X-Species 头的值是 dog,然而实际的结果却并非如此:
$ curl 'http://localhost:8080/test'
X-Species: cat
上面这个例子证明了即使运行在同一个请求处理阶段,分属不同模块的配置指令也可能会分开独立运行(除非像ngx_set_misc等模块那样针对ngx_rewrite模块提供特殊支持)。换句话说,在单个请求处理阶段内部,一般也会以Nginx模块为单位进一步地划分出内部子阶段。
第三方模块ngx_lua提供的rewrite_by_lua配置指令也和more_set_input_headers一样运行在rewrite阶段的末尾
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变量$a进行了自增计算。从该例的指令书写顺序上看,我们或许会期望输出是56,可是因为rewrite_by_lua会在所有的set语句之后执行,所以结果是57:
$ curl 'http://localhost:8080/test'
57
access阶段
在access阶段运行的配置指令多是执行访问控制性质的任务,比如检查用户的访问权限,检查用户的来源IP地址是否合法,诸如此类。
一、示例1
location /hello {
allow 127.0.0.1;
deny all;
echo "hello world";
}
这个/test接口被配置为只允许从本机(IP地址为保留的127.0.0.1)访问,而从其他IP地址访问都会被拒(返回403错误页)。
ngx_access模块自己的多条配置指令之间是按顺序执行的,直到遇到第一条满足条件的指令就不再执行后续的allow和deny指令。如果首先匹配的指令是allow,则会继续执行后续其他模块的指令或者跳到后续的处理阶段;而如果首先满足的是deny则会立即中止当前整个请求的处理,并立即返回403错误页。
结果展示:
$ curl 'http://localhost:8080/hello'
hello world
$ curl 'http://192.168.1.101:8080/hello'
<html>
<head><title>403 Forbidden</title></head>
<body bgcolor="white">
<center><h1>403 Forbidden</h1></center>
<hr><center>nginx</center>
</body>
</html>
为了避免阅读配置时的混乱,我们应该总是让指令的书写顺序和它们的实际执行顺序保持一致。
二、示例2
ngx_lua模块提供了配置指令access_by_lua,用于在access请求处理阶段插入用户Lua代码。这条指令运行于access阶段的末尾,因此总是在allow和deny这样的指令之后运行,虽然它们同属access阶段。一般我们通过access_by_lua在ngx_access这样的模块检查过客户端IP地址之后,再通过Lua代码执行一系列更为复杂的请求验证操作,比如实时查询数据库或者其他后端服务,以验证当前用户的身份或权限。
location /hello {
access_by_lua '
if ngx.var.remote_addr == "127.0.0.1" then
return
end
ngx.exit(403)
';
echo "hello world";
}
这里在Lua代码中通过引用Nginx标准的内建变量$remote_addr来获取字符串形式的客户端IP地址,然后用Lua的if语句判断是否为本机地址,即是否等于127.0.0.1.如果是本机地址,则直接利用Lua的return语句返回,让Nginx继续执行后续的请求处理阶段(包括 echo 指令所处的 content 阶段);而如果不是本机地址,则通过ngx_lua模块提供的Lua函数ngx.exit中断当前的整个请求处理流程,直接返回403错误页给客户端。
content阶段
一、示例1
content阶段是所有请求处理阶段中最为重要的一个,因为运行在这个阶段的配置指令一般都肩负着生成“内容”(content)并输出HTTP响应的使命。
location /test {
# rewrite phase
set $age 1;
rewrite_by_lua "ngx.var.age = ngx.var.age + 1";
# access phase
deny 10.32.168.49;
access_by_lua "ngx.var.age = ngx.var.age * 3";
# content phase
echo "age = $age";
}
测试结果:
$ curl 'http://localhost:8080/test'
age = 6
这个例子展示了通过同时使用多个处理阶段的配置指令来实现多个模块协同工作的效果。
在 rewrite 和 access 这两个阶段,多个模块的配置指令可以同时使用,譬如上例中的set指令和rewrite_by_lua指令同处rewrite阶段,而deny指令和access_by_lua指令则同处access阶段。但不幸的是,这通常不适用于content阶段。
绝大多数Nginx模块在向content阶段注册配置指令时,本质上是在当前的location配置块中注册所谓的“内容处理程序”(content handler)。每一个location只能有一个“内容处理程序”,因此,当在location中同时使用多个模块的content 阶段指令时,只有其中一个模块能成功注册“内容处理程序”。
我们应当避免在同一个 location 中使用多个模块的 content 阶段指令。
location /test {
echo hello;
echo world;
}
测试结果:
$ curl 'http://localhost:8080/test'
hello
world
这里使用多条echo指令是没问题的,因为它们同属ngx_echo模块,而且ngx_echo模块规定和实现了它们之间的执行顺序。值得一提的是,并非所有模块的指令都支持在同一个location中被使用多次。
location /test {
echo_before_body "before...";
proxy_pass http://127.0.0.1:8080/foo;
echo_after_body "after...";
}
location /foo {
echo "contents to be proxied";
}
在ngx_proxy模块返回的内容前后,ngx_echo模块的echo指令分别输出字符串"before..." 和 "after...",需要改用ngx_echo模块提供的echo_before_body和echo_after_body这两条配置指令。
测试结果:
$ curl 'http://localhost:8080/test'
before...
contents to be proxied
after...
配置指令echo_before_body和echo_after_body之所以可以和其他模块运行在content阶段的指令一起工作,是因为它们运行在 Nginx 的“输出过滤器”中。echo指令产生的“调试日志”时,Nginx在输出响应体数据时都会调用“输出过滤器”,所以ngx_echo模块才有机会在“输出过滤器”中对ngx_proxy模块产生的响应体输出进行修改(即在首尾添加新的内容)。值得一提的是,“输出过滤器”并不属于那11个请求处理阶段(毕竟许多阶段都可以通过输出响应体数据来调用“输出过滤器”)。
二、示例2
当一个 location 中未使用任何content阶段的指令,即没有模块注册“内容处理程序”时,content阶段会发生什么事情呢?
答案就是那些把当前请求的URI映射到文件系统的静态资源服务模块。当存在“内容处理程序”时,这些静态资源服务模块并不会起作用;反之,请求的处理权就会自动落到这些模块上。
Nginx一般会在content阶段安排三个这样的静态资源服务模块,按照它们在content阶段的运行顺序,依次是ngx_index模块,ngx_autoindex模块,以及ngx_static模块。
ngx_index和ngx_autoindex模块都只会作用于那些URI以/结尾的请求,例如请求GET /cats/,而对于不以/结尾的请求则会直接忽略,同时把处理权移交给content阶段的下一个模块。而ngx_static模块则刚好相反,直接忽略那些URI以/结尾的请求。
ngx_index 模块主要用于在文件系统目录中自动查找指定的首页文件,类似 index.html 和 index.htm 这样的
location / {
root /var/www/;
index index.htm index.html;
}
这样,当用户请求/地址时,Nginx就会自动在root配置指令指定的文件系统目录下依次寻找index.htm和index.html这两个文件。如果index.htm文件存在,则直接发起“内部跳转”到/index.htm这个新的地址;而如果index.htm文件不存在,则继续检查index.html是否存在。如果存在,同样发起“内部跳转”到/index.html;如果index.html文件仍然不存在,则放弃处理权给content阶段的下一个模块。
三、示例3
echo_exec指令和rewrite指令可以发起“内部跳转”。这种跳转会自动修改当前请求的URI,并且重新匹配与之对应的location配置块,再重新执行rewrite、access、content等处理阶段。因为是“内部跳转”,所以有别于HTTP协议中定义的基于302和301响应的“外部跳转”,最终用户的浏览器的地址栏也不会发生变化,依然是原来的URI位置。而ngx_index模块一旦找到了index指令中列举的文件之后,就会发起这样的“内部跳转”,仿佛用户是直接请求的这个文件所对应的URI一样。
location / {
root /var/www/;
index index.html;
}
location /index.html {
set $a 32;
echo "a = $a";
}
测试结果:
$ curl 'http://localhost:8080/'
a = 32
首先对于用户的原始请求GET /,Nginx匹配出location /来处理它,然后content阶段的ngx_index模块在/var/www/下找到了index.html,于是立即发起一个到/index.html位置的“内部跳转”。
在重新为/index.html这个新位置匹配location配置块时,location /index.html的优先级要高于location /,因为location块按照URI前缀来匹配时遵循所谓的“最长子串匹配语义”。这样,在进入location /index.html配置块之后,又重新开始执行rewrite 、access、以及content等阶段。最终输出a = 32。
在content阶段默认“垫底”的最后一个模块便是极为常用的ngx_static模块。这个模块主要实现服务静态文件的功能。比方说,一个网站的静态资源,包括静态.html文件、静态.css文件、静态.js文件、以及静态图片文件等等,全部可以通过这个模块对外服务。前面介绍的ngx_index模块虽然可以在指定的首页文件存在时发起“内部跳转”,但真正把相应的首页文件服务出去(即把该文件的内容作为响应体数据输出,并设置相应的响应头),还是得靠这个ngx_static模块来完成。