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