Nginx Rewrite研究筆記

原文出自:http://blog.cafeneko.info/2010/10/nginx_rewrite_note/php

在新主機的遷移過程當中,最大的困難就是WP permalink rewrite的設置.css

由於舊主機是用的Apache, 使用的是WP自己就能夠更改的.htaccess,沒有太大的難度.而此次在VPS上跑的是Nginx,主要是由於Nginx的速度比Apache要快不少.html

可是另外一方面就不是那麼舒服了,由於Nginx的rewrite跟Apache不一樣,並且是在服務器上面才能更改.nginx

下面是其間的一些研究筆記.(如下用例如無特別說明均摘自nginx wiki)web


/1 Nginx rewrite基本語法


Nginx的rewrite語法其實很簡單.用到的指令無非是這幾個正則表達式

  • set
  • if
  • return
  • break
  • rewrite

麻雀雖小,可御可蘿五臟俱全.只是簡單的幾個指令卻能夠作出絕對不輸apache的簡單靈活的配置.apache

1.setapi

set主要是用來設置變量用的,沒什麼特別的瀏覽器

2.ifbash

if主要用來判斷一些在rewrite語句中沒法直接匹配的條件,好比檢測文件存在與否,http header,cookie等,

用法: if(條件) {…}

- 當if表達式中的條件爲true,則執行if塊中的語句

- 當表達式只是一個變量時,若是值爲空或者任何以0開頭的字符串都會看成false

- 直接比較內容時,使用 = 和 !=

- 使用正則表達式匹配時,使用

~ 大小寫敏感匹配 
~* 大小寫不敏感匹配 
!~ 大小寫敏感不匹配 
!~* 大小寫不敏感不匹配

這幾句話看起來有點繞,總之記住: ~爲正則匹配, 後置*爲大小寫不敏感, 前置!爲」非」操做

隨便一提,由於nginx使用花括號{}判斷區塊,因此當正則中包含花括號時,則必須用雙引號將正則包起來.對下面講到的rewrite語句中的正則亦是如此. 
好比 「\d{4}\d{2}\.+」

- 使用-f,-d,-e,-x檢測文件和目錄

-f 檢測文件存在
-d 檢測目錄存在
-e 檢測文件,目錄或者符號連接存在
-x 檢測文件可執行

跟~相似,前置!則爲」非」操做

舉例

if ($http_user_agent ~ MSIE) {
  rewrite  ^(.*)$  /msie/$1  break;
}

//若是UA包含」MSIE」,rewrite 請求到/msie目錄下

if ($http_cookie ~* "id=([^;] +)(?:;|$)" ) {
  set  $id  $1;
}

//若是cookie匹配正則,設置變量$id等於正則引用部分

if ($request_method = POST ) {
  return 405;
}

//若是提交方法爲POST,則返回狀態405 (Method not allowed)

if (!-f $request_filename) {
  break;
  proxy_pass  http://127.0.0.1;
}

//若是請求文件名不存在,則反向代理localhost

if ($args ~ post=140){
  rewrite ^ http://example.com/ permanent;
}

//若是query string中包含」post=140″,永久重定向到example.com

3.return

return可用來直接設置HTTP返回狀態,好比403,404等(301,302不可用return返回,這個下面會在rewrite提到)

4.break

當即中止rewrite檢測,跟下面講到的rewrite的break flag功能是同樣的,區別在於前者是一個語句,後者是rewrite語句的flag

5.rewrite

最核心的功能(廢話)

用法: rewrite 正則 替換 標誌位

其中標誌位有四種

break – 中止rewrite檢測,也就是說當含有break flag的rewrite語句被執行時,該語句就是rewrite的最終結果 
last – 中止rewrite檢測,可是跟break有本質的不一樣,last的語句不必定是最終結果,這點後面會跟nginx的location匹配一塊兒提到 
redirect – 返回302臨時重定向,通常用於重定向到完整的URL(包含http:部分) 
permanent – 返回301永久重定向,通常用於重定向到完整的URL(包含http:部分)

由於301和302不能簡單的只單純返回狀態碼,還必須有重定向的URL,這就是return指令沒法返回301,302的緣由了. 做爲替換,rewrite能夠更靈活的使用redirect和permanent標誌實現301和302. 好比上一篇日誌中提到的Blog搬家要作的域名重定向,在nginx中就會這麼寫

rewrite ^(.*)$ http://newdomain.com/ permanent;

舉例來講一下rewrite的實際應用

rewrite  ^(/download/.*)/media/(.*)\..*$  $1/mp3/$2.mp3  last;

若是請求爲 /download/eva/media/op1.mp3 則請求被rewrite到 /download/eva/mp3/op1.mp3

使用起來就是這樣,很簡單不是麼? 不過要注意的是rewrite有不少潛規則須要注意

- rewrite的生效區塊爲sever, location, if

- rewrite只對相對路徑進行匹配,不包含hostname 好比說以上面301重定向的例子說明

rewrite ~* cafeneko\.info http://newdomain.com/ permanent;

這句是永遠沒法執行的,以這個URL爲例

http://blog.cafeneko.info/2010/10/neokoseseiki_in_new_home/?utm_source=rss&utm_medium=rss&utm_campaign=neokoseseiki_in_new_home

其中cafeneko.info叫作hostname,再日後到?爲止叫作相對路徑,?後面的一串叫作query string

對於rewrite來講,其正則表達式僅對」/2010/10/neokoseseiki_in_new_home」這一部分進行匹配,即不包含hostname,也不包含query string .因此除非相對路徑中包含跟域名同樣的string,不然是不會匹配的. 若是非要作域名匹配的話就要使用if語句了,好比進行去www跳轉

if ($host ~* ^www\.(cafeneko\.info)) {
  set $host_without_www $1;
  rewrite ^(.*)$ http://$host_without_www$1 permanent;
}

- 使用相對路徑rewrite時,會根據HTTP header中的HOST跟nginx的server_name匹配後進行rewrite,若是HOST不匹配或者沒有HOST信息的話則rewrite到server_name設置的第一個域名,若是沒有設置server_name的話,會使用本機的localhost進行rewrite

- 前面提到過,rewrite的正則是不匹配query string的,因此默認狀況下,query string是自動追加到rewrite後的地址上的,若是不想自動追加query string,則在rewrite地址的末尾添加?

rewrite  ^/users/(.*)$  /show?user=$1?  last;

rewrite的基本知識就是這麼多..但尚未完..還有最頭疼的部分沒有說…


/2 Nginx location 和 rewrite retry


nginx的rewrite有個很奇特的特性 — rewrite後的url會再次進行rewrite檢查,最多重試10次,10次後尚未終止的話就會返回HTTP 500

用過nginx的朋友都知道location區塊,location區塊有點像Apache中的RewriteBase,但對於nginx來講location是控制的級別而已,裏面的內容不只僅是rewrite.

這裏必須稍微先講一點location的知識.location是nginx用來處理對同一個server不一樣的請求地址使用獨立的配置的方式

舉例:

location  = / {
  ....配置A
}
 
location  / {
  ....配置B
}
 
location ^~ /images/ {
  ....配置C
}
 
location ~* \.(gif|jpg|jpeg)$ {
  ....配置D
}

訪問 / 會使用配置A 
訪問 /documents/document.html 會使用配置B 
訪問 /images/1.gif 會使用配置C 
訪問 /documents/1.jpg 會使用配置D

如何判斷命中哪一個location暫且按下不婊, 咱們在實戰篇再回頭來看這個問題.

如今咱們只須要明白一個狀況: nginx能夠有多個location並使用不一樣的配.

sever區塊中若是有包含rewrite規則,則會最早執行,並且只會執行一次, 而後再判斷命中哪一個location的配置,再去執行該location中的rewrite, 當該location中的rewrite執行完畢時,rewrite並不會中止,而是根據rewrite過的URL再次判斷location並執行其中的配置. 那麼,這裏就存在一個問題,若是rewrite寫的不正確的話,是會在location區塊間形成無限循環的.因此nginx纔會加一個最多重試10次的上限. 好比這個例子

location /download/ {
  rewrite  ^(/download/.*)/media/(.*)\..*$  $1/mp3/$2.mp3  last;
}

若是請求爲 /download/eva/media/op1.mp3 則請求被rewrite到 /download/eva/mp3/op1.mp3

結果rewrite的結果從新命中了location /download/ 雖然此次並無命中rewrite規則的正則表達式,但由於缺乏終止rewrite的標誌,其仍會不停重試download中rewrite規則直到達到10次上限返回HTTP 500

認真的朋友這時就會問了,上面的rewrite規則不是有標誌位last麼? last不是終止rewrite的意思麼?

說到這裏我就要抱怨下了,網上能找到關於nginx rewrite的文章中80%對last標誌的解釋都是

last – 基本上都用這個Flag

……這他媽坑爹呢!!! 什麼叫基本上都用? 什麼是不基本的狀況?  =皿=

有興趣的能夠放狗」基本上都用這個Flag」…

我最終仍是在stack overflow找到了答案:

last和break最大的不一樣在於

- break是終止當前location的rewrite檢測,並且再也不進行location匹配 
– last是終止當前location的rewrite檢測,但會繼續重試location匹配並處理區塊中的rewrite規則

仍是這個該死的例子

location /download/ {
  rewrite  ^(/download/.*)/media/(.*)\..*$  $1/mp3/$2.mp3  ;
  rewrite  ^(/download/.*)/movie/(.*)\..*$  $1/avi/$2.mp3  ;
  rewrite  ^(/download/.*)/avvvv/(.*)\..*$  $1/rmvb/$2.mp3 ;
}

上面沒有寫標誌位,請各位自行腦補…

若是請求爲 /download/acg/moive/UBW.avi

last的狀況是: 在第2行rewrite處終止,並重試location /download..死循環 
break的狀況是: 在第2行rewrite處終止,其結果爲最終的rewrite地址.

也就是說,上面的某位試圖下載eva op不但沒下到反而被HTTP 500射了一臉的例子正是由於用了last標誌因此纔會形成死循環,若是用break就沒事了.

location /download/ {
  rewrite  ^(/download/.*)/media/(.*)\..*$  $1/mp3/$2.mp3  break;
}

對於這個問題,我我的的建議是,若是是全局性質的rewrite,最好放在server區塊中並減小沒必要要的location區塊.location區塊中的rewrite要想清楚是用last仍是break.

有人可能會問,用break不就萬無一失了麼?

不對.有些狀況是要用last的. 典型的例子就是wordpress的permalink rewrite

常見的狀況下, wordpress的rewrite是放在location /下面,並將請求rewrite到/index.php

這時若是這裏使用break乃就掛了,不信試試. b( ̄▽ ̄)d…由於nginx返回的是沒有解釋的index.php的源碼…

這裏必定要使用last才能夠在結束location / 的rewrite, 並再次命中location ~ \.php$,將其交給fastcgi進行解釋.其後返回給瀏覽器的纔是解釋過的html代碼.

關於nginx rewrite的簡介到這裏就所有講完了,水平及其有限,請你們指出錯漏…


/3 實戰! WordPress的Permalink+Supercache rewrite實現


這個rewrite寫法實際上是來自supercache做者本家的某個評論中,網上很容易查到,作了一些修改. 先給出該配置文件的所有內容..部份內容碼掉了..絕對路徑什麼的你知道也沒啥用對吧?

server {
	listen   80;
	server_name  cafeneko.info www.cafeneko.info;
 
	access_log  ***;
	error_log   *** ;
 
	root   ***;
	index  index.php;
 
	gzip_static on;
 
	if (-f $request_filename) {
		break;
	}
 
	set $supercache_file '';
	set $supercache_uri $request_uri;
 
	if ($request_method = POST) {
		set $supercache_uri '';
	}
 
	if ($query_string) {
		set $supercache_uri '';
	}
 
	if ($http_cookie ~* "comment_author_|wordpress_logged_|wp-postpass_" ) {
		set $supercache_uri '';
	}
 
	if ($supercache_uri ~ ^(.+)$) {
		set $supercache_file /wp-content/cache/supercache/$http_host/$1index.html;
	}
 
	if (-f $document_root$supercache_file) {
		rewrite ^(.*)$ $supercache_file break;
	}
 
	if (!-e $request_filename) {
		rewrite . /index.php last;
	}
 
	location ~ \.php$ {
 
		fastcgi_pass   127.0.0.1:9000;
		fastcgi_index  index.php;
		fastcgi_param  SCRIPT_FILENAME  ***$fastcgi_script_name;
		include        fastcgi_params;
	}
 
	location ~ /\.ht {
		deny  all;
	}
}

下面是解釋:

gzip_static on;

若是瀏覽器支持gzip,則在壓縮前先尋找是否存在壓縮好的同名gz文件避免再次壓縮浪費資源,配合supercache的壓縮功能一塊兒使用效果最好,相比supercache原生的Apache mod_rewrite實現,nginx的實現簡單的多. Apache mod_rewrite足足用了兩套看起來如出一轍的條件判斷來分別rewrite支持gzip壓縮和不支持的狀況.

if (-f $request_filename) {
	break;
}

//若是是直接請求某個真實存在的文件,則用break語句中止rewrite檢查

set $supercache_file '';
set $supercache_uri $request_uri;

//用$request_uri初始化變量 $supercache_uri.

if ($request_method = POST) {
	set $supercache_uri '';
}

//若是請求方式爲POST,則不使用supercache.這裏用清空$supercache_uri的方法來跳過檢測,下面會看到

if ($query_string) {
	set $supercache_uri '';
}

//由於使用了rewrite的緣由,正常狀況下不該該有query_string(通常只有後臺纔會出現query string),有的話則不使用supercache

if ($http_cookie ~* "comment_author_|wordpress_logged_|wp-postpass_" ) {
	set $supercache_uri '';
}

//默認狀況下,supercache是僅對unknown user使用的.其餘諸如登陸用戶或者評論過的用戶則不使用.

comment_author是測試評論用戶的cookie, wordpress_logged是測試登陸用戶的cookie. wp-postpass不大清楚,字面上來看多是曾經發表過文章的?只要cookie中含有這些字符串則條件成立.

原來的寫法中檢測登陸用戶cookie用的是wordpress_,可是我在測試中發現登入/登出之後還會有一個叫wordpress_test_cookie存在,不知道是什麼做用,我也不清楚通常用戶是否會產生這個cookie.因爲考慮到登出之後這個cookie依然存在可能會影響到cache的判斷,因而把這裏改爲了匹配wordpress_logged_

if ($supercache_uri ~ ^(.+)$) {
	set $supercache_file /wp-content/cache/supercache/$http_host$1index.html;
}

//若是變量$supercache_uri不爲空,則設置cache file的路徑

這裏稍微留意下$http_host$1index.html這串東西,其實寫成 $http_host/$1/index.html 就好懂不少

以這個rewrite形式的url爲例

cafeneko.info/2010/09/tsukihime-doujin_part01/

其中 
$http_host = ‘cafeneko.info’ , $1 = $request_uri = ‘/2010/09/tsukihime-doujin_part01/’

則 $http_host$1index.html = ‘cafeneko.info/2010/09/tsukihime-doujin_part01/index.html’

而 $http_host/$1/index.html = ‘cafeneko.info//2010/09/tsukihime-doujin_part01//index.html’

雖然在調試過程當中二者並無不一樣,不過爲了保持正確的路徑,仍是省略了中間的/符號.

最後上例rewrite後的url = ‘cafeneko.info/wp-content/cache/supercache/cafeneko.info/2010/09/tsukihime-doujin_part01/index.html’

if (-f $document_root$supercache_file) {
	rewrite ^(.*)$ $supercache_file break;
}

//檢查cache文件是否存在,存在的話則執行rewrite,留意這裏由於是rewrite到html靜態文件,因此能夠直接用break終止掉.

if (!-e $request_filename) {
	rewrite . /index.php last;
}

//執行到此則說明不使用suercache,進行wordpress的permalink rewrite

檢查請求的文件/目錄是否存在,若是不存在則條件成立, rewrite到index.php

順便說一句,當時這裏這句rewrite看的我百思不得其解. .

只能匹配一個字符啊?這是什麼意思?

通常狀況下,想調試nginx rewrite最簡單的方法就是把flag寫成redirect,這樣就能在瀏覽器地址欄裏看到真實的rewrite地址.

然而對於permalink rewrite卻不能用這種方法,由於一旦寫成redirect之後,無論點什麼連接,只要沒有supercache,都是跳轉回首頁了.

後來看了一些文章才明白了rewrite的本質,實際上是在保持請求地址不變的狀況下,在服務器端將請求轉到特定的頁面.

乍一看supercache的性質有點像302到靜態文件,因此能夠用redirect調試.

可是permalink倒是性質徹底不一樣的rewrite,這跟wordpress的處理方式有關. 我研究不深就很少說了,簡單說就是保持URL不變將請求rewrite到index.php,WP將分析其URL結構再對其並進行匹配(文章,頁面,tag等),而後再構建頁面. 因此其實這條rewrite

rewrite . /index.php last;

說的是,任何請求都會被rewrite到index.php.由於」.」匹配任意字符,因此這條rewrite其實能夠寫成任何形式的能任意命中的正則.好比說

rewrite . /index.php last;
rewrite ^ /index.php last;
rewrite .* /index.php last;

效果都是同樣的,都能作到permalink rewrite.

最後要提的就是有人可能注意到個人rewrite規則是放在server塊中的.網上能找到的大多數關於wordpress的nginx rewrite規則都是放在location /下面的,可是上面我卻放在了server塊中,爲什麼?

緣由是WP或某個插件會在當前頁面作一個POST的XHR請求,原本沒什麼特別,但問題就出在其XHR請求的URL結構上.

正常的permalink通常爲: domain.com/year/month/postname/ 或者 domain.com/tags/tagname/ 之類.

但這個XHR請求的URL倒是 domain.com/year/month/postname/index.php 或者 domain.com/tags/tagname/index.php

這樣一來就命中了location ~ \.php$而交給fastcgi,但由於根本沒有作過rewrite其頁面不可能存在,結果就是這個XHR返回一個404

鑑於location之間匹配優先級的緣由,我將主要的rewrite功能所有放進了server區塊中,這樣就得以保證在進行location匹配以前是必定作過rewrite的.

這時有朋友又要問了,爲何命中的是location ~ \.php$而不是location / ?

…望天…長嘆…這就要扯到天殺的location匹配問題了….

locatoin並不是像rewrite那樣逐條執行,而是有着匹配優先級的,當一條請求同時知足幾個location的匹配時,其只會選擇其一的配置執行.

其尋找的方法爲:

1. 首先尋找全部的常量匹配,如location /, location /av/, 以相對路徑自左向右匹配,匹配長度最高的會被使用, 
2. 而後按照配置文件中出現的順序依次測試正則表達式,如 location ~ download\/$, location ~* \.wtf, 第一個匹配會被使用 
3. 若是沒有匹配的正則,則使用以前的常量匹配

而下面幾種方法當匹配時會當即終止其餘location的嘗試

1. = 徹底匹配,location = /download/ 
2. ^~ 終止正則測試,如location ^~ /download/ 若是這條是最長匹配,則終止正則測試,這個符號只能匹配常量 
3. 在沒有=或者^~的狀況下,若是常量徹底匹配,也會當即終止測試,好比請求爲 /download/ 會徹底命中location /download/而不繼續其餘的正則測試

總結:

1. 若是徹底匹配(無論有沒有=),嘗試會當即終止
2. 以最長匹配測試各個常量,若是常量匹配並有 ^~, 嘗試會終止 
3. 按在配置文件中出現的順序測試各個正則表達式 
4. 若是第3步有命中,則使用其匹配location,不然使用第2步的location

另外還能夠定義一種特殊的named location,以@開頭,如location @thisissparta 不過這種location定義不用於通常的處理,而是專門用於try_file, error_page的處理,這裏再也不深刻.

暈了沒? 用前文的例子來看看

location  = / {
  ....配置A
}
 
location  / {
  ....配置B
}
 
location ^~ /images/ {
  ....配置C
}
 
location ~* \.(gif|jpg|jpeg)$ {
  ....配置D
}

訪問 / 會使用配置A -> 徹底命中
訪問 /documents/document.html 會使用配置B -> 匹配常量B,不匹配正則C和D,因此用B 
訪問 /images/1.gif 會使用配置C -> 匹配常量B,匹配正則C,使用首個命中的正則,因此用C 
訪問 /documents/1.jpg 會使用配置D -> 匹配常量B,不匹配正則C,匹配正則D,使用首個命中的正則,因此用D

那麼再回頭看咱們剛纔說的問題.爲何那個URL結果奇怪的XHR請求會命中location ~ \.php$而不是location / ? 我相信你應該已經知道答案了.

因此要解決這個問題最簡單的方法就是把rewrite規則放在比location先執行的server塊裏面就能夠了喲.

此次的研究筆記就到此爲止了.

最後留一個思考題,若是不將rewrite規則放入server塊,還有什麼方法能夠解決這個XHR 404的問題?

原來的location /塊包含從location ~ \.php$到root爲止的部分.

答案是存在的.在用使用目前的方法前我死腦筋的在保留location /的前提下嘗試了不少種方法…請不要嘗試爲各類permalink構建獨立的location.由於wp的permalink種類不少,包括單篇文章,頁面,分類,tag,做者,存檔等等..歡迎在回覆中討論 /

參考:
Nginx wiki

-EOF-


更新  @2010.10.23

以前的supercache rewrite規則適用於大部分的WP.可是並不適用於mobile press插件的移動設備支持.

由於其中並無檢測移動設備的user agent,從而致使移動設備也會被rewrite到cache上.這樣的結果是在移動設備上也是看到的跟PC同樣的徹底版blog. 對於性能比較好的手機好比iphone安卓什麼的大概沒什麼問題,但比較通常的好比nokia上用opera mini等看就會比較辛苦了,此次把supercache本來在htaccess中的移動設備檢測的代碼塊也移植了過來.

在前文的配置文件中cookie檢測後面加入如下代碼段

	# Bypass special user agent
	if ($http_user_agent ~* "2.0 MMP|240x320|400X240|AvantGo|BlackBerry|Blazer|Cellphone|Danger|DoCoMo|Elaine/3.0|EudoraWeb|Googlebot-Mobile|hiptop|IEMobile|KYOCERA/WX310K|LG/U990|MIDP-2.|MMEF20|MOT-V|NetFront|Newt|Nintendo Wii|Nitro|Nokia|Opera Mini|Palm|PlayStation Portable|portalmmm|Proxinet|ProxiNet|SHARP-TQ-GX10|SHG-i900|Small|SonyEricsson|Symbian OS|SymbianOS|TS21i-10|UP.Browser|UP.Link|webOS|Windows CE|WinWAP|YahooSeeker/M1A1-R2D2|iPhone|iPod|Android|BlackBerry9530|LG-TU915 Obigo|LGE VX|webOS|Nokia5800") {
		set $supercache_uri '';
	}
 
	if ($http_user_agent ~* "w3c |w3c-|acs-|alav|alca|amoi|audi|avan|benq|bird|blac|blaz|brew|cell|cldc|cmd-|dang|doco|eric|hipt|htc_|inno|ipaq|ipod|jigs|kddi|keji|leno|lg-c|lg-d|lg-g|lge-|lg/u|maui|maxo|midp|mits|mmef|mobi|mot-|moto|mwbp|nec-|newt|noki|palm|pana|pant|phil|play|port|prox|qwap|sage|sams|sany|sch-|sec-|send|seri|sgh-|shar|sie-|siem|smal|smar|sony|sph-|symb|t-mo|teli|tim-|tosh|tsm-|upg1|upsi|vk-v|voda|wap-|wapa|wapi|wapp|wapr|webc|winw|winw|xda\ |xda-") {
		set $supercache_uri '';
	}

這樣就能夠對移動設備繞開cache規則,而直接使用mobile press產生的移動版的效果了.

相關文章
相關標籤/搜索