Nginx 的基础内置变量 / Nginx 重写 url 的模式

rewrite / try_files 指令

rewrite / try_files 都是对 $uri(不包含 $query_string) 进行处理,但 rewrite 会保持原请求 $query_stringtry_files 会丢弃,这也是为什么 try_files 重写时,通常都会加上 $query_string

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

这个简单,网站的根目录,没什么

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

$request_uri

请求的资源定位符。即你在浏览器中输入的原版 url(去掉主机),$request_uri 在整个请求会话中是固定不变的($uri 可能会因为重写规则被 nginx 重新定义,但$request_uri不会变)。注意:请求资源定位符是包含 queryString 的修饰的。

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

$uri

请求的资源名,和资源定位符$request_uri的区别是,资源定位符只是一个 symbol,可能会被映射重写,$uri 则是 nginx$request_uri 解析后所的出的资源名。

$uri 初始为不携带 $query_string 的 $request_uri,但 nginx 可能会对你的请求进行重写,重写处理后,最终的 $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
1、$request_uri = $uri = /news/index?p=1&ps=10
2、命中 location / { try_files $uri $uri/ /index.php$uri; }
3、重写解析
4、$request_uri = /news/index?p=1&ps=10
5、$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 生成的。

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;
1、请求:/news/index?p=1&ps=10
    $request_uri = /news/index?p=1&ps=10
    $uri = /news/index?p=1&ps=10
    
2、重写:/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
    
3、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'] 做路由和参数处理了。

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]
1、/uploads 是个目录,不是文件,不是脚本
2、/uploads/index.jpg 是个文件,那就是要执行的脚本了,后面的都是 pathinfo
3、然后你的 php 就把 index.jpg 当做脚本给执行了,index.jpg 里的 php 代码操作权限可是 站点root 级别的

修复漏洞
1、这种路径寻址在 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;
        }
    }
    ....
}

2、或者定义 location 规则,保护上传目录

location ~* /uploads/(.*\.php)([/|$]) {
    return 403;
}
阅读 1.1k

推荐阅读