網站在使用 Nginx 時都會進行個性化配置滿足自己的業務需要,而 URL 重寫幾乎是每個網站都必做的事情,Nginx 的 URL 重寫規則不像 Apache 那樣簡單直接,邏輯相對要複雜一些,本文將通過例子的方式幫助大家理解 Nginx rewrite 原理,希望能對您有些啟發。
Nginx 中重定向有多種方式:
外部重定向
return 指令返回 301 或 302(return 也可以返回其他狀態碼),可以放在 server 或 location 塊中。例如:
return 301 https://www.mi.com;or
return 302 https://www.mi.com;
還可以使用 rewrite 指令,例如:
rewrite ^/(.*)$ http://www.mi.com/$1;orrewrite ^/(.*)$ http://www.mi.com/ redirect;
內部重定向
return + error_page 指令的組合,或 try_files 指令和 rewrite 指令,非常靈活。
本文主要講解 rewrite 的工作原理,其他指令的使用方法大家可以自行查閱 Nginx 官網。在使用 Nginx 的 rewrite 指令時,flag 可以設置為 last 和 break,這兩個 flag 很容易混淆,後面我們會比較這兩個 flag 的區別,下面通過示例我們來認識一下 rewrite 指令。
Syntax: rewrite regex replacement [flag];Context: server, location, if
regex: 對請求的 URI 做正則匹配
replacement:目標 uri 匹配成功後替換的 url
可以使用的 flag 有以下 4 個(flag 也可以為空):
redirect:返回 302 臨時重定向,客戶端地址欄會顯示跳轉後的地址;
permanent:返回 301 永久重定向,客戶端地址欄會顯示跳轉後的地址;
last:內部重定向,停止處理後續 rewrite 模塊中的指令(客戶端無感知);
break:內部重定向,停止處理後續 rewrite 模塊中的指令(客戶端無感知)。
NOTE:
1. regex 匹配的是 uri,不包含 hostname 和 query string,默認 query string 是被追加到 replacement 末尾,如果不希望在末尾追加請求的 query string,可以在 replacement 的末尾加一個 "?"。
server { rewrite ^/mi_one/(.*)$ /mi/$1; rewrite ^/mi_two/(.*)$ /mi/$1?x=0&y=1?; location /mi/ {
echo $uri$is_args$args; }}$ curl "http://localhost/mi_one/hello?a=1&b=2"
output: /mi/hello?a=1&b=2
$ curl "http://localhost/mi_two/hello?a=1&b=2"
output: /mi/hello?x=0&y=1
2. 如果 replacement 是以 "http://","https://" 或 $scheme" 開始的字符串,那麼 rewrite 指令停止後面的處理,直接返回給客戶端,沒有指定 flag 時,與 redirect 效果相同。
3. 沒有 flag 的 rewrite 指令根據出現的順序執行,flag 可以控制指令的執行順序。
4. 在配置中開啟 rewrite_log 指令,日誌文件中會記錄 rewrite 的匹配過程,有助於調試 rewrite 問題。
在講 rewrite 前我們先來簡單了解下 Nginx 請求處理流程,為什麼需要了解請求處理流程呢?因為 rewrite 操作與其中幾個 phase 關係很密切,熟悉了請求處理流程,理解 rewrite 執行邏輯就會很容易。在 Nginx 內部將請求處理劃分為 11 個 phase,每個 phase 會執行對應的 handler,這裡我們不打算逐個進行講解。在 11 個 phase 中與 rewrite 指令邏輯有關的只有 4 個,所以在本文我們主要關注 SERVER_REWRITE、FIND_CONFIG、REWRITE 和 POST_REWRITE 這四個 phase。
首先我們要清楚的是:
server 塊中的 rewrite 模塊指令在 SERVER_REWRITE 階段解析;
location 塊中的 rewrite 模塊指令在 REWRITE 階段解析;
SERVER_REWRITE - 請求到達後首先處理這個階段的 rewrite 指令操作
FIND_CONFIG - 根據 SERVER_REWRITE 階段得到的 uri 查找 location
REWRITE - 確定 location 後執行 locaton 中 rewrite 操作
POST_REWRITE - 根據上一階段的 uri 重寫結果做決策,可能跳回 FIND_CONFIG 階段重新查找 location,也可能繼續執行後邊的 phase。例如:在 location 中配置了 rewrite 指令並且指定 flag=break,執行完本條 rewrite 終止後邊的 rewrite 匹配,然後執行 PREACCESS 階段中的 handler。同樣的場景下 flag=last,執行完本條 rewrite 終止後邊的 rewrite 匹配,然後跳到 FIND_CONFIG 階段再次查找 location。未指定 flag 的情況與 flag=last 類似,唯一區別是在同一層級中未指定 flag 的 rewrite 語句不會終止後續的 rewrite 匹配。
未指定 flag 的 rewrite 會按照出現順序進行匹配,server 塊中 rewrite 匹配完以後根據改寫的 uri 查找 location,然後再匹配 location 中的 rewrite,location 中的 rewrite 指令匹配成功後會再次查找 location。
server { rewrite ^/(.*)$ /mi_one/$1; rewrite ^/mi_one/(.*)$ /mi_two/$1; location / {
echo "This is default location"; } location /mi_one/ {
echo "This is mi_one location"; } location /mi_two/ {
echo "This is mi_two location"; }}$ curl http://localhost/mi_zero/helloThis is mi_two location
說明:
匹配第一條 rewrite 成功,uri 被改寫為 / mi_one/mi_zero/hello,沒有指定 flag 的 rewrite 繼續匹配後面的 rewrite
匹配第二條 rewrite 成功,此時 uri 被改寫為 / mi_two/mi_zero/hello
查找 location,mi_two 被確定為最終的 location
server { rewrite ^/(.*)$ /mi_one/$1; location / {
echo "This is default location"; } location /mi_one/ { rewrite ^/mi_one/(.*)$ /mi_two/$1;
echo "This is mi_one location"; } location /mi_two/ {
echo "This is mi_two location"; }}$ curl http://localhost/helloThis is mi_two location
說明:
匹配 server 塊中的 rewrite 成功,uri 被改寫為 / mi_one/hello
server 塊中只有一條 rewrite 指令,開始查找 location
location mi_one 被找到,開始匹配 location 中 rewrite
location 中的 rewrite 匹配成功,uri 被改寫為 / mi_two/hello
再次查找 location,mi_two 被確定為最終使用的 location
再來看一個例子:
server { rewrite ^/(.*)$ http://www.mi.com/$1; rewrite ^/mi_one/(.*)$ /mi_two/$1; location / {
echo "This is default location"; } location /mi_one/ {
echo "This is mi_one location"; } location /mi_two/ {
echo "This is mi_two location"; }}$ curl -I http://localhost/mi_one/hellohttp code: 302
Location: http://www.mi.com/mi_one/hello
說明:
匹配第一條 rewrite 成功,由於 replacement 是以 http:// 開始的字符串,所以 rewrite 指令直接返回給客戶端 302,並且停止匹配後續的 rewrite。
指定 flag 為 redirect 時,rewrite 匹配成功後直接返回給客戶端 302,不會繼續匹配後續的 rewrite。
server { rewrite ^/(.*)$ /mi_one/$1 redirect; rewrite ^/mi_one/(.*)$ /mi_two/$1; rewrite ^/mi_two/(.*)$ /mi_three/$1; location / {
echo "This is default location"; } location /mi_one/ {
echo "This is mi_one location"; } location /mi_two/ {
echo "This is mi_two location"; } location /mi_three/ {
echo "This is mi_three location"; }}$ curl -I http://localhost/mi_zero/hellohttp code: 302
Location: http://localhost/mi_one/mi_zero/hello$ curl -I http://localhost/mi_one/hellohttp code: 302
Location: http://localhost/mi_one/mi_one/hello$ curl -I http://localhost/mi_two/hellohttp code: 302
Location: http://localhost/mi_one/mi_two/hello
再來看一個例子:
server { rewrite ^/mi_one/(.*)$ /mi_two/$1; rewrite ^/mi_two/(.*)$ /mi_three/$1 redirect; rewrite ^/(.*)$ /mi_one/$1; location / {
echo "This is default location"; } location /mi_one/ {
echo "This is mi_one location"; } location /mi_two/ {
echo "This is mi_two location"; } location /mi_three/ {
echo "This is mi_three location"; }}$ curl -I http://localhost/mi_one/hellohttp code: 302
Location: http://localhost/mi_three/hello$ curl -I http://localhost/mi_two/hellohttp code: 302
Location: http://localhost/mi_three/hello$ curl http://localhost/mi_zero/helloThis is mi_one location
指定 flag=permanent 時,與 redirect 效果相同,唯一的區別 http code 返回 301。
rewrite 的 last 和 break 這兩個 flag 使用場景很多並且也很容易混淆,他們的共同點都會停止當前層級後續的 rewrite 匹配,區別需要分兩種情況:第一種使用在 server block 中,last 和 break 沒有區別。第二種使用在 location block 中,last 會根據改寫的 uri 重新查找 location,break 不會重新查找 location,而是在當前 location 中執行後續的指令。(註:last 和 break 不僅停止 rewrite 的匹配,同時還會停止 Nginx rewrite 模塊中其他指令的執行,例如:set、return 指令)
server { rewrite ^/mi_one/(.*)$ /mi_two/$1 last; rewrite ^/mi_two/(.*)$ /mi_three/$1; location / {
echo "This is default location"; } location /mi_one/ {
echo "This is mi_one location"; } location /mi_two/ {
echo "This is mi_two location"; } location /mi_three/ {
echo "This is mi_three location"; }}$ curl http://localhost/mi_one/helloThis is mi_two location
說明:
匹配第一條 rewrite 成功,uri 被改寫為 / mi_two/hello,由於 flag 指定為 last 會停止後續的 rewrite 的匹配(僅停止 server block 中的 rewrite 匹配),所以會根據改寫的 uri 查找 location
再來看一個 last 在 location block 中使用的例子:
server { rewrite ^/(.*)$ /mi_one/$1; location / {
echo "This is default location"; } location /mi_one/ { rewrite ^/(.*)$ /mi_three/$1 last; rewrite ^/mi_three/(.*)$ /;
echo "This is mi_one location"; } location /mi_two/ {
echo "This is mi_two location"; } location /mi_three/ {
echo "This is mi_three location"; }}$ curl http://localhost/helloThis is mi_three location
說明:
匹配 server block 中第一條 rewrite 成功,uri 被改寫為 / mi_one/hello
查找 location,mi_one 被確定為使用的 location
匹配 location block 中的 rewrite,location 中的第一條 rewrite 匹配成功,uri 被改寫為 / mi_three/mi_one/hello,由於 flag 指定為 last,所以停止 location 中後續的 rewrite 匹配,此時再根據 uri=/mi_three/mi_one/hello 查找 location,最終 mi_three 被確定為使用的 location
server { rewrite ^/mi_one/(.*)$ /mi_two/$1 break; rewrite ^/mi_two/(.*)$ /mi_three/$1; location / {
echo "This is default location"; } location /mi_one/ {
echo "This is mi_one location"; } location /mi_two/ {
echo "This is mi_two location"; } location /mi_three/ {
echo "This is mi_three location"; }}$ curl http://localhost/mi_one/helloThis is mi_two location
說明:
匹配 server block 中第一條 rewrite 成功,uri 被改寫為 / mi_two/hello,由於 flag 指定為 break 所以會停止 server block 中後續的 rewrite 匹配,根據 uri=/mi_two/hello 查找 location,最終 mi_two 被確定為使用的 location
server { rewrite ^/(.*)$ /mi_one/$1; location / {
echo "This is default location"; } location /mi_one/ { rewrite ^/(.*)$ /mi_three/$1 break; rewrite ^/mi_two/(.*)$ /;
echo "This is mi_one location";
echo "uri: ${uri}"; } location /mi_two/ {
echo "This is mi_two location"; } location /mi_three/ {
echo "This is mi_three location"; }}$ curl http://localhost/helloThis is mi_one locationuri: /mi_three/mi_one/hello
說明:
匹配 server block 中第一條 rewrite 成功,uri 被改寫為 / mi_one/hello
根據 uri=/mi_one/hello 查找 location,mi_one 被確定為使用的 location
匹配 location block 中的 rewrite,location 中的第一條 rewrite 匹配成功,uri 被改寫為 / mi_three/mi_one/hello,由於 flag 指定為 break,所以停止 location 中後續的 rewrite 匹配,並且把當前 location 作為最終使用的 location,不會重新查找 location(last 會繼續查找 location)
在 Nginx 的配置中可以實現簡單的編程,理解起來相對有點難度,通過閱讀此文希望能對你有些啟發,能夠根據項目需求可以配置更複雜的 rewrite 規則。想要更深入的理解 rewrite,還需要大家自己動手實踐。
https://github.com/agile6v/awesome-nginx
http://nginx.org/en/docs/http/ngx_http_rewrite_module.html
https://www.nginx.com/blog/creating-nginx-rewrite-rules/
https://www.nginx.com/blog/converting-apache-to-nginx-rewrite-rules
http://www.thegeekstuff.com/2017/08/nginx-rewrite-examples/
http://winginx.com/en/htaccess
https://w3techs.com/technologies/overview/web_server/all
http://nginx.org/en/docs/dev/development_guide.html#httpphases