寫在前面的話
我們前面已經談了編譯安裝,基本語法,日志處理,location 匹配,root / alias 的不同效果。這里我們主要談談 rewrite(重寫)功能,順便說說 nginx 中自帶的變量。在談日志格式的時候我們已經聊了一些,這里做個補充。
指令:rewrite
rewrite 的實現依賴於我們編譯的時候的 PCRE 庫,我們可以通過 rewrite 功能實現將 URL 重寫的功能。
總的來說,rewrite 能夠實現以下:
1. 用戶請求到達某個 server ,如果滿足 server 內部的 rewrite 的正則匹配,那么 rewrite 將會對用戶請求 URI 重寫。
2. 重寫完成后直接在該 server 內部去匹配 location。
3. 當匹配到 location 后,如果 location 內部又有 rewrite,那執行 rewrite 后再次在這個 server 內部去匹配 location,直到請求返回。
4. 當然這個過程不是無限的,nginx 對於這樣的跳轉就支持 10 次,如果過多甚至死循環,則會報 500 錯誤。
基本語法格式:
rewrite regex replacement [flag];
說明:
這里的使用 regex 匹配 URI,並將匹配到的 URI 替換成新的 URI(replacement)。如果有多個 rewrite,執行順序是從上到下依次執行,匹配到一個后匹配並不會終止,會繼續匹配下去,直到返回最后一個匹配為止。如果想中途終止,則需要設置 flag 參數。
當然上面說的都是重寫 URI,如果 placement 中包含了任何協議相關,如:http:// 和 https://,則請求就直接返回 302 重定向終止了。
當然,瀏覽器在接收到 30x 的狀態碼后,會再度根據這個返回去請求 rewrite 之后的地址,最終得到所要想要的結果。如果不是 30x 的狀態碼,則屬於 nginx 內部跳轉,瀏覽器不需要再度發起請求。
在 rewrite 中有 4 個 flag 參數:
參數 | 說明 |
---|---|
last | 停止所有 rewrite 相關指令,然后使用新的 URI 進行 location 匹配。 |
break | 停止所有 rewrite 相關指令, 和 last 不同的是,last 接着繼續使用新的 URI 匹配 location。 而 break 則是直接使用當前的 URI 進行請求處理,能避免重復 rewrite,last 一般在 server,break 一般在 location。 |
redirect | URI 中不包含協議如 https://,但依然希望它返回 30x,讓瀏覽器二次請求然后獲取到結果就需要 redirect。 |
permanent | 和 redirect 類似,但是直接返回 301 永久重定向。 |
在 rewrite 中常用的正則:
表達式 | 作用 |
---|---|
. | 匹配換行符以外任意字符 |
? | 重復0次或者1次 |
+ | 重復1次或多次 |
* | 重復0次或者多次 |
\d | 匹配數字 |
^ | 匹配開始 |
$ | 匹配結束 |
{n} | 重復 n 次 |
{n,} | 重復 n 次或更多次 |
[c] | 匹配單個字符c |
[a-z] | 匹配 a-z 任意一個小寫字母 |
使用 () 可以將匹配內容括起來,后面使用 $1 來引用,當然,第二個 () 就是 $2。
rewrite 示例:
示例1:直接跳轉到其它 URL,但是將參數帶過去
在 vhosts 下面新建:rewrite-demo.conf
server { listen 8083; server_name localhost; rewrite_log on; rewrite ^/(.*) https://www.ezops.com/$1 permanent; error_log /data/logs/nginx/rewrite-error.log; access_log /data/logs/nginx/rewrite-access.log mylog; }
我們這里開啟 rewrite log,這樣定向錯誤會記錄到 error_log 中。配置完成后重載 nginx,訪問:
結果如下:
示例2:測試 last 和 break,修改剛剛的配置,這里我們用到 nginx 自帶變量 uri
server { listen 8083; server_name localhost; rewrite_log on; rewrite ^/(.*) /hello/$1 last; error_log /data/logs/nginx/rewrite-error.log; access_log /data/logs/nginx/rewrite-access.log mylog; location ^~ /hello { echo "URI 1: $uri"; rewrite ^/hello/(.*) /world/$1 last; echo "URI 2: $uri"; } location ^~ /world { echo "URI 3: $uri"; } }
重載訪問:
此時把 location 中 last 改為 break 測試:
可以發現:
如果 rewrite 是 last 作 flag 並不影響接下來繼續去匹配 location,且該 location 下面就執行了 rewrite 操作,其它的都沒有執行到。
但是當 break 作 flag 時,rewrite 就終止於目前的這個 location 了,在完成重寫 URI 之后就開始執行該 location 下面的其他操作了。
示例3:參數后面 ? 的作用測試,修改配置,我們用到另外一個變量 args
server { listen 8083; server_name localhost; rewrite_log on; error_log /data/logs/nginx/rewrite-error.log; access_log /data/logs/nginx/rewrite-access.log mylog; location ^~ /hello { rewrite ^/(.*) /world/?from=$1 break; echo "URI: $uri"; echo "ARG: $args"; } }
訪問結果如下:
修改 rewrite 配置,添加 ?:rewrite ^/(.*) /world/?from=$1? break; 重載訪問:
可以發現:
如果 replacement 中包含參數,那默認舊 URI 中的請求參數也會拼接到 replacement 后面作為新的 URI,如果不希望這樣,只需在 replacement 的后面加上 ?。
指令:set
我們一直都在說,nginx 為我們提供了很多的內部變量,但是有些時候這些變量並不能滿足我們的需求,我們需要其它的一些自定變量來協助我們完成一個比較復雜的需求。set 就是這樣一個指定,用來定義屬於我們自己的變量,它的基本語法如下:
set $variable value;
舉個例子,我們在 vhosts 下新增配置:set-demo.conf
server { listen 8084; server_name localhost; set $STEP 1; location / { set $STEP $STEP-2; echo $STEP; } }
重載配置,訪問測試:
指令:if 和 try_files
在 nginx 中,我們也可以像在其它編程語言一樣添加邏輯判斷,其中就有 if 和 try_files,if 一般在舊版中使用,但是新版中並不影響。語法格式:
if (判斷條件) {...}
判斷只能在 server 和 location 中使用。
1. 當判斷條件只是一個變量的時候,只有該變量的值為空或者 0 的時候才為 false。
2. 變量可以通過 = 或者 != 來判斷,如:$var = 123。
3. 判斷條件里面也可以是一個正則匹配,如:$var ~ regex。
4. 其它的一些文件,目錄校驗符號,如:-d / -f / -e / -x。
文件校驗符如下:
符號 | 作用 |
---|---|
-f | 檢驗文件是否存在,可以取反:!-f |
-d | 檢驗目錄是否存在 |
-e | 檢驗文件/目錄/鏈接文件是否存在 |
-x | 檢驗文件是否為可執行文件 |
舉個例子測試,在 vhosts 目錄下創建:if-demo.conf
server { listen 8085; server_name localhost; set $VAR 1; set $STEP $VAR; location / { if ($VAR) { echo "URL 0: VARIABLE TEST 0"; set $STEP $STEP-0; } if ($VAR = 1) { echo "URL 1: VARIABLE TEST 1"; set $STEP $STEP-1; } if ($http_user_agent ~* Mozilla) { echo "URL 2: BROWSER"; set $STEP $STEP-2; } if ($http_user_agent ~ curl) { echo "URL 3: COMMAND"; set $STEP $STEP-3; } if (-f /tmp/test.txt) { echo "URL 4: FILE EXIST"; set $STEP $STEP-4; } if (!-f /tmp/test.txt) { echo "URL 5: FILE NOT EXIST"; set $STEP $STEP-5; echo "STEP: $STEP"; } } }
我們定義了兩個變量,VAR 和 STEP,VAR 用於測試判斷,STEP 用於記錄執行了哪些 if,重載訪問測試:
可以發現:
我們明明執行了 0 1 3 5 這 4 個 if 判斷,但是真正執行的 echo 的卻只有最后一個 5。
if 常常被我們用來做客戶端驗證,比如我們一個網站,如果是電腦打開,我們讓他跳轉到電腦版,手機打開跳轉到手機版。
try_files 其實就是 if 語句精簡版,但是個人其實更喜歡 if 一點,所有對 try_files 感興趣的可以詳細的了解一下,我們這里舉個簡單的例子:
location / { root /data/www/demo; index index.html index.htm; try_files $uri $uri/ @rewrites; } location @rewrites { rewrite ^(.+)$ /index.html last; }
比如:用戶訪問 http://192.168.100.111/hello/world,那么 $uri 就是 /hello/world,那么 try_files 就會去指定的 root 下查找這個文件是否存在,如果存在則直接返回,如果不存在就訪問第二個參數,還不存在就繼續,直到最后一個參數,我們把它跳轉到對應的 location 上面。在 try_files 會自行判斷是文件還是目錄。
雖然很方便,但是個人還是更喜歡 if 一些!
注意:
在 if 中不支持嵌套,也不支持 else,嵌套 if 可以使用多個 if 來實現它。
指令:return
停止一切處理,返回結果給客戶端,如果返回的狀態碼是 444,則斷開 TCP 連接,不發送任何東西。
如果不帶狀態碼直接返回 URL 則被視為 302。簡單示例:
在 vhosts 下面新建:return-demo.conf
server { listen 8086; server_name localhost; location / { if ($http_user_agent ~ curl) { return 200 'COMMAND USER\n'; } if ($http_user_agent ~ Mozilla) { return 302 http://www.baidu.com?$args; } return 404; } }
命令行測試:
瀏覽器訪問:http://192.168.100.111:8086/hello?user=world
綜合示例
我們這里做一個結合前面的知識點一起完成的一個示例:
server { listen 8087; server_name localhost; default_type text/html; # 默認來源 PC / 移動端 set $MACHINE pc; if ( $http_user_agent ~* "(mobile|nokia|iphone|ipad|android|samsung|htc|blackberry)" ){ set $MACHINE mobile; } # 多重判斷變量設計 set $FROM pc; set $TAG ''; location ^~ /pc/ { # 拼接來源和請求 URI 頭 set $FROM pc; set $TAG $MACHINE-$FROM; # 根據來源和 URI 頭采取不同的處理 if ($TAG = pc-pc) { rewrite '^/pc/([a-z0-9]{2})/([a-z0-9]{2})/(.*)\.(png|jpg|gif)$' /data?file=$3.$4 last; } if ($TAG = mobile-pc) { return 302 http://m.jd.com?$args; } return 200 '<h1>PC INDEX WEB!</h1>'; } location ^~ /mobile/ { # 拼接來源和請求 URI 頭 set $FROM mobile; set $TAG $MACHINE-$FROM; # 根據來源和 URI 頭采取不同的處理 if ($TAG = pc-mobile) { return 302 http://www.baidu.com?$args; } if ($TAG = mobile-mobile) { return 200 '<h1>WELCOME TO MOBILE WEB!</h1>'; } return 200 '<h1>MOBILE INDEX WEB!</h1>'; } location ^~ /data { root /data/www/images; try_files /$arg_file /image404.html; } location = /image404.html { return 200 '<h1>IMAGE NOT FOUND!</h1>'; } # 默認處理 location / { if ($MACHINE = pc) { return 200 '<h1>PC DEFAULT WEB</h1>'; } if ($MACHINE = mobile) { return 200 '<h1>MOBILE DEFAULT WEB</h1>'; } } }
說明:
1. 我們將訪問區分電腦端和移動端,當電腦端訪問默認端口的時候顯示:
2. 當 uri 部分以 /pc/ 開頭,則返回電腦的默認頁面:
3. 當電腦端訪問 /mobile/ 開頭並帶有參數,則保留參數跳轉到百度,如訪問: http://192.168.100.111:8087/mobile/test?name=hello
4. 我們新建 /data/www/images 目錄並上傳 login.png 作為測試,此時電腦端訪問指定匹配規則的圖片返回圖片:
5. 訪問不存在的圖片報錯:
6. 移動端一個意思,這里就不做演示。
7. 配置中包含了多重判斷,我們使用一個中間變量了存儲作為判斷區分。
8. 最后值得注意的是,我們使用 return 返回 HTML 文本,需要定義默認類型:default_type text/html; 否則會變成下載文件。
自帶變量
nginx 自帶了很多變量,可以參考如下表:我們以請求 http://www.baidu.com/hello/world?name=dylan&age=25 為例
變量 | 說明 |
---|---|
$args | 請求中的完整參數。就是示例中:name=dylan&age=25 |
$arg_PARAMETER | 獲取指定參數,如:$arg_name,就能獲取到 name 參數 |
$binary_remote_addr | 二進制客戶端地址,如:\x0A\xE0B\x0E |
$body_bytes_sent | 向客戶端發送 HTTP 響應中包體部分的字節數,前面日志中用過 |
$content_length | 客戶端請求頭部中 Content_Length 字段 |
$content_type | 客戶端請求頭部中 Content_Type 字段 |
$cookie_COOKIE | 獲取指定 cookie 的值 |
$document_root | 當前請求所使用的 root 配置項的值 |
$uri | 當前請求的 uri,不帶任何參數 |
$document_uri | 與 uri 含義相同 |
$request_uri | 原始請求 uri,帶完整的參數。$uri 和 $document_uri 可能是內部重定向后的。 |
$host | 請求頭部 Host 字段,字段不存在,則以實際 server(虛擬主機)名稱代替。 |
$hostname | nginx 所在機器的名稱。 |
$http_HEADER | 當前 HTTP 請求相應頭部的值,全小寫 |
$sent_http_HEADER | 返回客戶端 HTTP 響應中相應頭部的值 |
$is_args | 請求中的 uri 是否帶參數,如果帶參數,值為 ?,如果不帶參數,值為空 |
$limit_rate | 當前連接限速為多少,0 表示不限速 |
$nginx_version | 當前 nginx 的版本號 |
$query_string | 請求 uri 中的參數,與 args 相同,只讀的 |
$remote_addr | 客戶端地址 |
$remote_port | 客戶端連接使用端口 |
$remote_user | 客戶端連接使用賬戶,使用 auth basic module 時定義的用戶名 |
$request_filename | 請求中 uri 經過 root 或 alias 轉換以后的路徑 |
$request_body | HTTP 請求中的包體,該參數只在 proxy_pass 或 fastcgi_pass 中有意義 |
$request_body_file | HTTP 請求中的包體存儲的臨時文件名 |
$request_completion | 請求全部完成時,值為"ok",若沒完成,就返回客戶端,值為空 |
$request_method | HTTP 請求的方法名,如get,put,post |
$scheme | scheme,如請求:https://www.baidu.com,值為 https |
$server_addr | 服務器地址 |
$server_name | 服務器名稱 |
$server_port | 服務器端口 |
$server_protocol | 服務器向客戶端發送響應的協議,如 HTTP/1.1 或者 HTTP/1.0 表示不限速 |
紅色部分為常用的變量!
小結
rewrite 是一個很實用的功能,能夠解決我們很多問題,但是同時我們又不推薦把這個做的太為復雜,不適合維護。同樣,if set 也是我們日常用的比較多的。這其中牽扯到正則表達式。可能需要多花點時間。