Nginx 的基礎內置變量 / Nginx 重寫 url 的模式

rewrite / try_files 指令

rewrite / try_files 都是對 $uri(不包含 $query_string) 進行處理,但 rewrite 會保持原請求 $query_stringtry_files 會丟棄,這也是爲何 try_files 重寫時,一般都會加上 $query_stringphp

location / {
    if (!-e $request_filename) {
        # rewrite 處理的是 $uri,^(.*)$ 匹配出來的 $1 其實就是 $uri
        rewrite ^(.*)$ /index.php$1 last;
        rewrite ^(.*)$ /index.php$uri last; # 這樣寫也沒區別
        break;
    }
    
    # 非 pathinfo 重寫
    # 適用於使用 $_SERVER['request_uri'] 中的路徑作路由解析的框架 laravel/yii2
    # 因此重寫不傳遞 $uri 也不要緊
    # 但必須得加上 $is_args$query_string,不然 $_GET 就空了
    try_files $uri $uri/ /index.php$is_args$query_string;
    
    # pathinfo 重寫
    # 適用於使用 $_SERVER['path_info'] 作路由解析的框架 thinkphp
    # $is_args$query_string 能夠不加
    # 由於規範的 pathinfo 要求參數也路徑化在 $uri 中了
    # 不須要 $_GET 參數
    try_files $uri $uri/ /index.php$uri$is_args$query_string;
}

$document_root

這個簡單,網站的根目錄,沒什麼nginx

server {
    root /home/wwwroot/site/public;
}

$request_uri

請求的資源定位符。即你在瀏覽器中輸入的原版 url(去掉主機),$request_uri 在整個請求會話中是固定不變的($uri 可能會由於重寫規則被 nginx 從新定義,但$request_uri不會變)。注意:請求資源定位符是包含 queryString 的修飾的。laravel

/index.php/news/index?p=1&ps=10
/news/index?p=1&ps=10

$uri

請求的資源名,和資源定位符$request_uri的區別是,資源定位符只是一個 symbol,可能會被映射重寫,$uri 則是 nginx$request_uri 解析後所的出的資源名。thinkphp

$uri = 不帶 $query_string 參數的請求的最終資源名,由於 nginx 可能會對你的 $request_uri 進行重寫,起初的 $uri = $request_uri,但重寫處理後,最終的 $uri 可能就與 $request_uri 不一樣了,因此 $uri 在沒有發生重寫時就是 $request_uri 去掉可能攜帶的 $query_string,發生了重寫處理就要看重寫後最終的資源名了。瀏覽器

# 重寫規則
location / {
    try_files $uri $uri/ /index.php$uri;
}

location ~ [^/]\.php(/|$) {
    ....
}

# 直接命中 location 沒有發生重寫
/index.php/news/index?p=1&ps=10
$request_uri = /index.php/news/index?p=1&ps=10
$uri = /index.php/news/index

# 命中了 / 觸發了 try_files 中的重寫
/news/index?p=1&ps=10
一、$request_uri = $uri = /news/index?p=1&ps=10
二、命中 location / { try_files $uri $uri/ /index.php$uri; }
三、重寫解析
四、$request_uri = /news/index?p=1&ps=10
五、$uri = /index.php/news/index

$query_string / $args & $is_args

urlqueryString 參數,$query_string$args 與其徹底一致,$is_args 是友好的表示是否攜帶了 queryString,攜帶爲'?' 未攜帶 ''服務器

/news/list?p=1&ps=10
$query_string = $args = p=1&ps=10
$is_args = ?

# 重寫時使用 $is_args 追加 $query_string 更爲規範
location / {
    try_files $uri $uri/ /index.php$is_args$query_string;
}

$request_filename

計算表達式:$request_filename = $document_root$uri
請求的資源文件路徑,這個變量是在你的 $request_uri 被解析處理好後獲得了最終的 $uri,才結合 $document_root 生成的。yii2

server {
    root /home/wwwroot/site/public/index.php/news/list;
}

/index.php/new/list?p=1&ps=10
$request_uri = /index.php/news/list?p=1&ps=10
$uri = /index.php/new/list
$request_filename = $document_root$uri = /home/wwwroot/site/public/index.php/news/list

fastcgi_script_name / fastcgi_path_info

這兩個變量放一塊兒說比較好,默認狀況下,$fastcgi_script_name = $uri,但咱們的 url 爲了美觀大都採用了 pathinfo 風格。因此,若是直接把 /index.php/news/index 傳遞給 php,那 $_SERVER['SCRIPT_NAME'] = '/index.php/news/index' 了。而通過了 pathinfo 處理:框架

# fastcgi_split_path_info 指令會作以下處理
# 將 $1 賦值給 $fastcgi_script_name
# 將 $2 賦值給 $fastcgi_path_info
# 這樣咱們就得到可真正要執行的 php 腳本名 腳本文件名 和 腳本後攜帶的路徑
fastcgi_split_path_info ^(.+?\.php)(/.+)$;
fastcgi_param SCRIPT_NAME $fastcgi_script_name;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_param PATH_INFO $fastcgi_path_info;
fastcgi_param PATH_TRANSLATED $document_root$fastcgi_path_info;
一、請求:/news/index?p=1&ps=10
    $request_uri = /news/index?p=1&ps=10
    $uri = /news/index?p=1&ps=10
    
二、重寫:/index.php/news/index?p=1&ps=10
    $uri = /index.php/news/index
    $request_filename = /home/wwwroot/site/public/index.php/news/index
    $fastcgi_script_name = $uri
    
三、pahtinfo 解析
    fastcgi_split_path_info ^(.+?\.php)(/.+)$;
    $fastcgi_script_name = /index.php
    $fastcgi_path_info = /news/index
    
    # 給 php 的一些 $_SERVER 變量填充
    fastcgi_param  SCRIPT_NAME        $fastcgi_script_name;
    fastcgi_param  SCRIPT_FILENAME    $document_root$fastcgi_script_name;
    fastcgi_param  PATH_INFO          $fastcgi_path_info;
    fastcgi_param  PATH_TRANSLATED    $document_root$fastcgi_path_info;

咱們就可使用 $_SERVER['PATH_INFO'] 作路由和參數處理了。yii

url 模式

如今不少開發者可能直接使用框架入門,框架給予效率的同時,也可能讓開發者們忽略的不少底層細節,好比對於單入口文件類型的框架,全部請求的資源文件其實都只有 index.php,而框架負責將你輸入的 "資源路徑" 進行解析,路由,處理。函數

常規的 url

/index.php?ctrl=news&action=index&p=1&ps=10

框架實際理解和處理的 url 則如上,框架最終總會把你的 url 解析映射至相應的 controller & actionnginx$uri/index.php

資源化的 url

/index.php/news/index?p=1&ps=10
/news/index?p=1&ps=10

框架:yii2 / laravel。請求資源 路徑 化,查詢參數 仍是常規方式。對於 seo 的爬蟲來講,服務上存在一個資源:/news/indexnginx$uri/index.php/news/index/news/index,比 常規的 url 更爲友好。

pathinfo 化的 url

/news/index/p/1/ps/10

框架:thinkphp。查詢參數也 路徑 化,對於 seo 的爬蟲來講,服務上存在一個資源:/news/index/p/1/ps/10nginx$uri/news/index/p/1/ps/10

各種框架的 url 模式

pathinfourl 模式,thinkphp 徹底支持,傳入和生成 url 都不須要作特別的處理。yii2 / laravel路由聲明 並不是 100% 支持,沒辦法作到 queryString 自動 pathinfo 。對於 yii2 / laravel 來講,queryString 參數也 pathinfo 化,但 路由聲明 在一些場景下會很冗餘(參數少而固定還好,多且不固定就尷尬了)。

Thinkphp

TPpathinfo 模式的 url,能夠解析和處理 pathinfo 化的 queryString,使用框架的 url 助手方法生成 pathinfo 風格的連接。
爲何說 TPpathinfo 很推崇呢,官方給出的 url 重寫規則就是不攜帶 queryStringtry_files 重寫。因此,TP 是在告訴你,queryString 參數也要 pathinfo 化到 $uri 中。

# pathinfo 模式的重寫規則
location / {
    # try_files 本質上不是重寫,你傳什麼它請求什麼,這裏只傳了 $uri,沒有 $query_string
    # 那請求再入時,$query_string 就是空的了
    try_files $uri $uri/ /index.php$uri;
    
    # 兼容老版本的 nginx
    if (!-e $request_filename) {
        # 二選一 沒區別
        # rewrite 只處理 $uri(沒有$query_string)
        # ^(.+)$ 正則的也是 $uri 因此 $1 裏沒有 $query_string
        # 但 rewrite 會隱式保持請求上下文裏的 $query_string 即使重寫沒傳遞
        # 在重寫後的請求裏 $query_string 仍是有的
        rewrite ^(.+)$ /index.php$1 last; break;
        rewrite $uri /index.php$uri last; break;
    }
}

url 助手函數生成的連接風格

/?s=/news/index&p=1&ps=10 //普通模式
/news/index/p/1/ps/10 //pathinfo模式

Yii2

Yii2prettyUrl 並不是徹底的 pathinfo 模式,只是把請求的"資源文件"路徑化,資源文件的描述參數 queryString 依然仍是使用常規模式。

prettyUrl 模式的重寫規則

location / {
    # try_files 必需要加上 $query_string 不加 get 參數就丟了
    try_files $uri $uri/ /index.php$is_args$query_string;
    
    # 兼容老版本的 nginx
    # rewrite 加不加 $query_string 都同樣 上下文會保持
    if (!-e $request_filename) {
        rewrite ^(.+)$ /index.php last; break;
        rewrite $uri /index.php last; break;
    }
}

有沒有注意到 yii2 非必須攜帶 $uri 轉發?下面的 laravel 也是如此。由於 yii2/laravel 的路由解析的是 $request_uri,重寫的 url 只是用來把 queryString 傳遞給框架,框架內部會使用 nginx::$request_uri => php::$_SERVER['REQUEST_URI'] 中的路徑信息進行路由解析。

/news/index?p=1&ps=10
nginx::$request_uri = /news/index?p=1&ps=10
php::$_SERVER['REQUEST_URI'] = /news/index?p=1&ps=10
$routePath = '/news/index'
$routeCtrl = 'news'
$routeAction = 'index'
$requestQueryString = $_GET
'urlManager'   => [
    'enablePrettyUrl'     => true,
    'showScriptName'      => false,// Url 助手生成連接時隱藏 /index.php
    'enableStrictParsing' => true,
    'rules'               => [
        [
            'class'      => 'yii\rest\UrlRule',
            'controller' => ['v1/news'],
            'pluralize'  => false,//自動複數
        ],
        'GET /<controller:\w+>'              => '<controller>/index',
        'GET /<controller:\w+>/<id:\d+>'     => '<controller>/detail',
        'POST /<controller:\w+>'             => '<controller>/create',
        'PUT /<controller:\w+>/<id:\d+>'     => '<controller>/createOrUpdate',
        'PATCH /<controller:\w+>/<id:\d+>'   => '<controller>/update',
        'DELETE /<controller:\w+>/<id:\d+>'  => '<controller>/delete',
        'OPTIONS /<controller:\w+>/<id:\d+>' => '<controller>/options',
        'HEAD /<controller:\w+>/<id:\d+>'    => '<controller>/head',
        '/'                                  => 'site/default', // default route
    ]
]

url 助手函數生成的連接風格

Url::to(["/news/index", "p" => 1, "ps" => 10])
// enablePrettyUrl = false
/index.php?r=news/index&p=1&ps=20
// enablePrettyUrl = true
/news/index?p=1&ps=10

Laravel

Laravelyii2 相似,並不是 yii2 / laravel 不能 pathinfo,而是說 TPpathinfo 親和力比較高,能夠友好的解析和生成 urlyii2 / laravel 須要聲明 url,若是咱們想 pathinfo 化的參數比較多,那聲明 url 就比較坑了。

// yii2 /new/index/1/10
'GET <controller:\w+>/<action:\w+>/<p:\d+>/<ps:\d+>' => '<controller>/<action>'

// laravel /new/index/1/10 laravel
Route::get('/news/index/{p?}/{ps?}', function ($p = 1, $ps = 10) {
})->where(['p' => '\d+', 'ps' => '\d+'])->name('news.index');
// /news/index/2
url('news.index', ['p' => 2]);

cgi.fix_pathinfo 漏洞

簡單講一下 phpcgi.fix_pathinfo = 1 時潛在的漏洞,思考下面的url在沒有過多處理(估計如今很多線上的網站都沒過多處理)會觸發什麼場景

# 在上傳目錄 /uploads/ 下上傳了一張把 index.php 改成 index.jpg 的 "圖片"
hackUrl: /uploads/index.jpg/hack.php/your/site

# location 規則
location ~ [^/]\.php(/|$)
{
    fastcgi_pass 127.0.0.1:9000;
    fastcgi_index  index.php;
    include fastcgi.conf;
    include pathinfo.conf;
}

這個 hackUrl 是能夠命中給出的 location 規則的,hack.php 的目的就是被上面的 location 規則捕獲。

而後 nginx 解析獲得的 script_namepathinfo 分別是

fastcgi_split_path_info ^(.+?\.php)(/.+)$;
SCRIPT_NAME /uploads/index.jpg/hack.php
PATH_INFO /your/site

fastcgiSCRIPT_NAME 傳遞給 php 後,php 發現其並不是有效的腳本文件,fix_pathinfo = 1 時,自動修正 pathinfo,開始以下嘗試

/uploads/index.jpg/hack.php splitTo [/uploads, /index.jpg, /hack.php]
一、/uploads 是個目錄,不是文件,不是腳本
二、/uploads/index.jpg 是個文件,那就是要執行的腳本了,後面的都是 pathinfo
三、而後你的 php 就把 index.jpg 當作腳本給執行了,index.jpg 裏的 php 代碼操做權限但是 站點root 級別的

修復漏洞
一、這種路徑尋址在 nginx 層就能攔截掉
$request_filename 最終會指向一個肯定的 php 腳本資源,咱們把其中的 php 腳本文件名提取出來,若是服務器上不存在這個 php 文件就當即返回。這樣就能夠關閉惡意使用攜帶 .phpurl 去觸發捕獲了。

location ~ [^/].php(/|$) {
    if ($request_filename ~* (.*\.php)) {
        set $php_script_name $1;
        if (!-e $php_script_name) {
            return 403;
        }
    }
    ....
}

二、或者定義 location 規則,保護上傳目錄

location ~* /uploads/(.*\.php)([/|$]) {
    return 403;
}
相關文章
相關標籤/搜索