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;
}
相关文章
相关标签/搜索