Nginx 的 Http 模塊介紹(上)
本部分內容將詳細介紹 Nginx 中對 Http請求的 11 個處理階段,分成 3 個小節講解并進行相關實驗操作。
1. http 請求 11 個處理階段介紹
Nginx 將一個 Http 請求分成多個階段,以模塊為單位進行處理。其將 Http請求的處理過程分成了 11 個階段,各個階段可以包含任意多個 Http 的模塊并以流水線的方式處理請求。這 11 個 Http 階段如下所示:
typedef enum {
NGX_HTTP_POST_READ_PHASE = 0,
NGX_HTTP_SERVER_REWRITE_PHASE,
NGX_HTTP_FIND_CONFIG_PHASE,
NGX_HTTP_REWRITE_PHASE,
NGX_HTTP_POST_REWRITE_PHASE,
NGX_HTTP_PREACCESS_PHASE,
NGX_HTTP_ACCESS_PHASE,
NGX_HTTP_POST_ACCESS_PHASE,
NGX_HTTP_TRY_FILES_PHASE,
NGX_HTTP_CONTENT_PHASE,
NGX_HTTP_LOG_PHASE
} ngx_http_phases;
網上有人做了一個非常形象的圖片,如下圖所示。我們可以看到 11 個階段的處理順序,以及每個階段中涉及到的相關模塊以及模塊之間的順序。
1.1 POST_READ 階段
POST_READ 階段是 Nginx 接收到 Http 請求完整頭部后的處理階段,這里主要使用的是 realip 模塊獲取用戶的真實地址,方便后續對該 IP 進行限速或者過濾其請求等。
1.2 SERVER_REWRITE 和 REWRITE 階段
SERVER_REWRITE 和后面的 REWRITE 階段一般是使用 rewrite 模塊修改 Http請求的 uri,實現請求的控制。
1.3 FIND_CONFIG 階段
FIND_CONFIG 階段只是做 location 的匹配項。
1.4 PREACCESS、ACCESS 和 POST_ACCESS 階段
PREACCESS、ACCESS 和 POST_ACCESS 是和 Http 請求訪問權限相關的階段。PREACCESS 階段是在連接之前要做的訪問控制, 這個階段有 limit_conn 和 limit_req 等模塊工作。ACCESS 階段是解決用戶能不能訪問,比如根據用戶名、密碼限制用戶訪問(auth_basic 模塊)、根據 ip 限制用戶訪問(access 模塊)以及第三方模塊認證限制用戶的訪問(auth_request模塊)。POST_ACCESS 是在 ACCESS 之后要做的一些工作。
1.5 TRY_FILES 階段
TRY_FILES 階段為訪問靜態文件資源而設置的。有時候又稱之為 PRECONTENT 階段,即在 CONTENT 階段之前做的事情。主要是 try_files 模塊在此階段工作。
1.6 CONTENT
最重要的 CONTENT 是處理 Http 請求內容的階段,大部分 HTTP 模塊介入這個階段,比如 index、autoindex、concat 以及反向代理的模塊都是在這里生效的。
1.7 LOG 階段
LOG 是處理完請求后的日志記錄階段,如 access_log 模塊。
Tips: 所有的 Http請求必須都是從上到下,一個接一個階段執行的。
2. realip 模塊
realip 模塊是在 postread 階段生效的,它的作用是:當本機的 nginx 處于一個反向代理的后端時獲取到真實的用戶 ip。 如果沒有 realip 模塊,Nginx 中的 $remote_addr 可能就不是客戶端的真實 ip 了,而是代理主機的 ip。
realip模塊的配置實例如下:
set_real_ip_from 10.10.10.10;
# real_ip_recursive off;
real_ip_recursive on;
real_ip_header X-Forwarded-For;
set_real_ip_from 是指定我們信任的后端代理服務器,real_ip_header 是告訴 nginx 真正的用戶 ip 是存在 X-Forwarded-For 請求頭中的。
當 real_ip_recursive 設置為 off 時,nginx 會把 real_ip_header 指定的 Http頭中的最后一個 ip 當成真實 ip;
而當 real_ip_recursive 為 on 時,nginx 會把 real_ip_header 指定的 Http頭中的最后一個不是信任服務器的 ip (前面設置的set_real_ip_from)當成真實 ip。通過這樣的手段,最后拿到用戶的真實 ip。
3. rewrite 模塊
rewrite 模塊可以看到它在 SERVER_REWRITE 和 REWRITE 階段都有介入。rewrite 模塊的主要功能是改寫請求的 uri。它是 Nginx 默認安裝的模塊。rewrite 模塊會根據正則匹配重寫 uri,然后發起內部跳轉再匹配 location, 或者直接做30x重定向返回客戶端。rewrite 模塊的指令有 break, if, return, rewrite, set 等,這些都是我們常用到的。
3.1 return 指令
Syntax: return code [text];
# return code URL;
# return URL;
Default: —
Context: server, location, if
return 指令返回后,Http 請求將在 return 的階段終止,后續階段將無法進行,所以許多模塊得不到執行。
return 200 "hello, world"
3.2 rewrite 指令
Syntax: rewrite regex replacement [flag];
Default: --
Context: server, location, if
1、將 regex 指定的 url 替換成 replacement 這個新的 url,可以使用正則表達式及變量提取。
2、當 replacement 以 http:// 或者 https:// 或者 $schema 開頭,則直接返回 302 重定向
3、替換后的 url 根據 flag 指定的方式進行處理
- last: 用 replacement 這個 url 進行新的 location 匹配
- break: break 指令停止當前腳本指令的執行
- redirect:返回 302 重定向
- permanent: 返回 301 重定向
3.3 if 指令
Syntax: if (condition) { ... }
Default: —
Context: server, location
if 指令的條件表達式:
- 檢查變量是否為空或者為 0
- 將變量與字符串做匹配,使用 = 或者 !=
- 將變量與正則表達式做匹配:
- ~ 或者 !~ 大小寫敏感
- ~* 或者 !~* 大小寫不敏感
- 檢查文件是否存在 -f 或者 !-f
- 檢查目錄是否存在 -d 或者 !-d
- 檢查文件、目錄、軟鏈接是否存在 -e !-e
- 是否為可執行文件 -x 或者 !-x
實例
if ($request_medthod = POST){
return 405;
}
if($invalid_refer){
return 403;
}
4. location 匹配
location 匹配是在 FIND_CONFIG 階段進行的,我們需要掌握 location 的匹配規則和匹配順序。
4.1 location 匹配規則
規則 | 匹配 |
---|---|
= | 嚴格匹配。如果請求匹配這個 location,那么將停止搜索并立即處理此請求 |
~ | 區分大小寫匹配(可用正則表達式) |
~* | 不區分大小寫匹配(可用正則表達式) |
!~ | 區分大小寫不匹配 |
!~* | 不區分大小寫不匹配 |
^~ | 前綴匹配 |
@ | “@” 定義一個命名的location,使用在內部定向時 |
/ | 通用匹配,任何請求都會匹配到 |
4.2 location 匹配順序
- “=” 精準匹配,如果匹配成功,則停止其他匹配
- 普通字符串指令匹配,優先級是從長到短(匹配字符越多,則選擇該匹配結果)。匹配成功的location如果使用^~,則停止其他匹配(正則匹配)
- 正則表達式指令匹配,按照配置文件里的順序(從上到下),成功就停止其他匹配
- 如果正則匹配成功,使用該結果;否則使用普通字符串匹配結果
有一個簡單總結如下:
(location =) > (location 完整路徑) > (location ^~ 路徑) > (location ,* 正則順序) > (location 部分起始路徑) > (location /)
即:
(精確匹配)> (最長字符串匹配,但完全匹配) >(非正則匹配)>(正則匹配)>(最長字符串匹配,不完全匹配)>(location通配)
5. 實驗
5.1 realip 模塊使用
realip 模塊默認沒有被編譯進 Nginx 的,我們需要在源碼編譯階段使用–with-http_realip_module,將 realip 模塊編譯進來后方可使用。接下來,我們做個簡單測試,首先準備一個 server 塊如下:
server {
listen 8007;
server_name localhost;
set_real_ip_from 218.19.206.164;
real_ip_recursive off;
# real_ip_recursive on;
real_ip_header X-Forwarded-For;
location / {
return 200 "client real ip: $remote_addr\n";
}
}
首先,我們將 real_ip_recursive 設置為 off,然后做一次請求:
$ curl -H "X-Forwarded-For: 1.1.1.1,218.19.206.164" http://主機ip:8007
client real ip: 218.19.206.164
這里返回的是頭部參數 X-Forwarded-For 中最后一個 ip,如果將 real_ip_recursive 設置為 on,此時,由于 set_real_ip_from 中設置218.19.206.164為信任的方向代理 ip,那么 Nginx 會往前找一位,認為 1.1.1.1 是用戶的真實ip。
$ ./nginx -s reload
$ curl -H "X-Forwarded-For: 1.1.1.1,218.19.206.164" http://主機ip:8007
client real ip: 1.1.1.1
5.2 return 指令和 if 指令聯合使用
我們寫一個簡單配置如下:
server {
server_name return_and_if.test.com;
listen 8008;
root html;
# 404錯誤跳轉到403.html頁面,根路徑由root指令指定
error_page 404 /403.html;
# return 405 '405 Not Allowed!\n';
location / {
if ( $request_method = POST ) {
return 200 "Post Request!\n";
}
}
}
先測試if指令,當請求方法為 POST 時,我們能得到 ‘post request!’ 這樣的字符串輸出。GET 請求時候,針對 404 情況,會跳轉到/403.html,我們準備一個 403.html 頁面,里面寫上’403, forbidden!’ 這一行內容,開始下面的 Http 請求:
$ curl -XPOST http://180.76.152.113:8008
Post Request!
$ curl http://180.76.152.113:8008/a.txt
403, forbidden!
如果我們打開 return 405 這行指令,則 error_page 將不會生效,連同后面的 location 匹配也不會生效。無論我們發送如何請求,都會返回405的錯誤信息。這是因為 server 中的 return 指令是在 SERVER_REWRITE中執行的,而 location 匹配則是在下一個階段 FIND_CONFIG 中執行的,所以上一個階段在 return 后,根本不會進入后面的階段執行。
$ curl http://180.76.152.113:8009
405 Not Allowed!
5.3 rewrite 模塊使用
首先,我們準備環境,首先是新建一個目錄 third(全路徑為/root/test/third),再該目錄下新建一個文件 3.txt, 里面只有一行內容 ‘hello, world’。接下來,我們準備一個 server 塊,加到 Http 指令塊中:
server {
server_name rewrite.test.com;
listen 8009;
# 打開rewrite日志,可以看到對應的rewrite結果
rewrite_log on;
error_log logs/rewrite_error.log notice;
root /root/test/;
location /first {
rewrite /first/(.*) /second/$1 last;
return 200 'first!';
}
location /second {
rewrite /second/(.*) /third/$1 break;
# rewrite /second/(.*) /third/$1;
return 200 'second!';
}
location /third {
return 200 'third!';
}
}
上述配置中,要打開 rewrite_log指令,這樣我們可以看到 rewrite 指令的相應日志,方便查看結果。
當我們在 /second 配置中,使用 break 時,請求命令:
$ curl http://主機ip:8009/first/3.txt
hello, world
如果是不使用 break 標識,則請求結果如下:
$ curl http://主機ip:8009/first/3.txt
second!
首先是 /first/3.txt 請求在 /first 中匹配,并被替換為 /second/3.txt, last 標識表示將繼續進行匹配,在 /second 中,uri 又被 rewrite 成 /third/3.txt, 如果后面跟了 break 標識,表示 rewrite 到此結束,不會執行后面的 return 指令,直接請求靜態資源 /third/3.txt,得到其內容’hello, world’;如果是沒有 break 標識,則會在執行 return 指令后直接返回,并不會繼續執行下去,最后返回’second!'字符串。
5.4 location 匹配
server {
server_name location.test.com;
listen 8010;
location = / {
return 200 "精確匹配/";
}
location ~* /ma.*ch {
return 200 "正則匹配/ma.*ch";
}
location ~ /mat.*ch {
return 200 "正則匹配/match.*";
}
location = /test {
return 200 "精確匹配/test";
}
location ^~ /test/ {
return 200 "前綴匹配/test";
}
location ~ /test/he*o {
return 200 "正則匹配/test/he*o";
}
location / {
return 200 "通配/";
}
}
我們按照這樣的 location 規則,進行匹配實驗,結果如下:
# 精確匹配優先級最高
$ curl http://localhost:8010/
精確匹配/
$ curl http://localhost:8010/test
精確匹配/test
# 前綴匹配優先級高于正則匹配
$ curl http://180.76.152.113:8010/test/heeo
前綴匹配/test
# 正則匹配,按照順序依次匹配,如果同時匹配兩個正則,則前面的優先匹配
$ curl http://180.76.152.113:8010/matxxch
正則匹配/ma.*ch
# 什么都匹配不到時,最后匹配通配/
$ curl http://180.76.152.113:8010/xxxxx
通配/
6. 小結
這里介紹了 Nginx 處理 Http 請求的 11 個階段,并重點介紹了 前三個階段POST_READ、REWRITE以及FIND_CONFIG以及這些階段中涉及到的模塊和指令。前面講到的指令都是 Nginx 中的高頻指令,必須要熟練掌握。