Nginx 配置指令的執行順序(十)
運行在 post-rewrite
階段之后的是所謂的 preaccess
階段。該階段在 access
階段之前執行,故名preaccess
.
標準模塊 ngx_limit_req 和 ngx_limit_zone 就運行在此階段,前者可以控制請求的訪問頻度,而后者可以限制訪問的并發度。這里我們僅僅和它們打個照面,后面還會有機會專門接觸到這兩個模塊。
前面反復提到的標準模塊 ngx_realip 其實也在這個階段注冊了處理程序。有些讀者可能會問:“這是為什么呢?它不是已經在 post-read
階段注冊處理程序了嗎?”我們不妨通過下面這個例子來揭曉答案:
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 的配置指令放在了 location
配置塊中。前面我們介紹過,Nginx 匹配 location
的動作發生在 find-config
階段,而 find-config
階段遠遠晚于 post-read
階段執行,所以在 post-read
階段,當前請求還沒有和任何 location
相關聯。在這個例子中,因為ngx_realip 的配置指令都寫在了 location
配置塊中,所以在 post-read
階段,ngx_realip 模塊的處理程序沒有看到任何可用的配置信息,便不會執行來源地址的改寫工作了。
為了解決這個難題,ngx_realip 模塊便又特意在 preaccess
階段注冊了處理程序,這樣它才有機會運行location
塊中的配置指令。正是因為這個緣故,上面這個例子的運行結果才符合直覺預期:
$ 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";
}
}
這里,我們在 rewrite
階段將 $remote_addr 的值保存到了用戶變量 $addr
中,然后再輸出。因為 rewrite
階段先于 preaccess
階段執行,所以當 ngx_realip 模塊尚未在 preaccess
階段改寫來源地址時,最初的來源地址就已經在 rewrite
階段被讀取了。上例的實際請求結果證明了我們的結論:
$ 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 當時讀出來的值。
而第二行調試信息
[debug] 32488#0: *1 http script set $addr
則顯示我們對變量 $addr
進行了賦值操作。
后面兩行信息
[debug] 32488#0: *1 realip: "1.2.3.4"
[debug] 32488#0: *1 realip: 0100007F FFFFFFFF 0100007F
是 ngx_realip 模塊在 preaccess
階段改寫當前請求的來源地址。我們看到,改寫后的新地址確實是期望的1.2.3.4
. 但很明顯這個操作發生在 $addr
變量賦值之后,所以已經太遲了。
而最后一行信息
[debug] 32488#0: *1 http script var: "127.0.0.1"
則是 echo 配置指令在輸出時讀取變量 $addr
時產生的,我們看到它的值是改寫前的來源地址。
看到這里,有的讀者可能會問:“如果 ngx_realip 模塊不在 preaccess
階段注冊處理程序,而在rewrite
階段注冊,那么上例不就可以工作了?”答案是:不一定。因為 ngx_rewrite 模塊的處理程序也同樣注冊在 rewrite
階段,而前面我們在 (二) 中特別提到,在這種情況下,不同模塊之間的執行順序一般是不確定的,所以 ngx_realip 的處理程序可能仍然在 set 語句之后執行。
一個建議是:盡量在 server
配置塊中配置 ngx_realip 這樣的模塊,以避免上面介紹的這種棘手的例外情況。
運行在 preaccess
階段之后的則是我們的另一個老朋友,access
階段。前面我們已經知道了,標準模塊ngx_access、第三方模塊 ngx_auth_request 以及第三方模塊 ngx_lua 的 access_by_lua 指令就運行在這個階段。
access
階段之后便是 post-access
階段。從這個階段的名字,我們也能一眼看出它是緊跟在 access
階段后面執行的。這個階段也和 post-rewrite
階段類似,并不支持 Nginx 模塊注冊處理程序,而是由 Nginx 核心自己完成一些處理工作。post-access
階段主要用于配合 access
階段實現標準 ngx_http_core 模塊提供的配置指令 satisfy 的功能。
對于多個 Nginx 模塊注冊在 access
階段的處理程序,satisfy 配置指令可以用于控制它們彼此之間的協作方式。比如模塊 A 和 B 都在 access
階段注冊了與訪問控制相關的處理程序,那就有兩種協作方式,一是模塊 A 和模塊 B 都得通過驗證才算通過,二是模塊 A 和模塊 B 只要其中任一個通過驗證就算通過。第一種協作方式稱為 all
方式(或者說“與關系”),第二種方式則被稱為 any
方式(或者說“或關系”)。默認情況下,Nginx 使用的是 all
方式。下面是一個例子:
location /test {
satisfy all;
deny all;
access_by_lua 'ngx.exit(ngx.OK)';
echo something important;
}
這里,我們在 /test
接口中同時配置了 ngx_access 模塊和 ngx_lua 模塊,這樣 access
階段就由這兩個模塊一起來做檢驗工作。其中,語句 deny all
會讓 則總是允許訪問。當我們通過 satisfy 指令配置了 all
方式時,就需要access
階段的所有模塊都通過驗證,但不幸的是,這里 ngx_access 模塊總是會拒絕訪問,所以整個請求就會被拒:
$ curl localhost:8080/test
<html>
<head><title>403 Forbidden</title></head>
<body bgcolor="white">
<center><h1>403 Forbidden</h1></center>
<hr><center>nginx</center>
</body>
</html>
細心的讀者會在 Nginx 錯誤日志文件中看到類似下面這一行的出錯信息:
[error] 6549\#0: *1 access forbidden by rule
然而,如果我們把上例中的 satisfy all
語句更改為 satisfy any
,
location /test {
satisfy any;
deny all;
access_by_lua 'ngx.exit(ngx.OK)';
echo something important;
}
結果則會完全不同:
$ curl localhost:8080/test
something important
即請求反而最終通過了驗證。這是因為在 any
方式下,access
階段只要有一個模塊通過了驗證,就會認為請求整體通過了驗證,而在上例中,ngx_lua 模塊的 access_by_lua 語句總是會通過驗證的。
在配置了 satisfy any
的情況下,只有當 access
階段的所有模塊的處理程序都拒絕訪問時,整個請求才會被拒,例如:
location /test {
satisfy any;
deny all;
access_by_lua 'ngx.exit(ngx.HTTP_FORBIDDEN)';
echo something important;
}
此時訪問 /test
接口才會得到 403 Forbidden
錯誤頁。這里,post-access
階段參與了 access
階段各模塊處理程序的“或關系”的實現。
值得一提的是,上面這幾個的例子需要 ngx_lua 0.5.0rc19 或以上版本;之前的版本是不能和 satisfy any
配置語句一起工作的。