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模塊來完成。
