rewrite / try_files 指令
rewrite / try_files
都是对 $uri(不包含 $query_string)
进行处理,但 rewrite
会保持原请求 $query_string
,try_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
url
的 queryString
参数,$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
& action
。nginx
的 $uri
是 /index.php
。
资源化的 url
/index.php/news/index?p=1&ps=10
/news/index?p=1&ps=10
框架:yii2
/ laravel
。请求资源 路径
化,查询参数
还是常规方式。对于 seo 的爬虫来说,服务上存在一个资源:/news/index
,nginx
的 $uri
是 /index.php/news/index
或 /news/index
,比 常规的 url
更为友好。
pathinfo 化的 url
/news/index/p/1/ps/10
框架:thinkphp
。查询参数也 路径
化,对于 seo 的爬虫来说,服务上存在一个资源:/news/index/p/1/ps/10
,nginx
的 $uri
是 /news/index/p/1/ps/10
。
各类框架的 url 模式
pathinfo
的 url
模式,thinkphp
完全支持,传入和生成 url
都不需要做特别的处理。yii2
/ laravel
的 路由声明
并非 100% 支持,没办法做到 queryString
自动 pathinfo
。对于 yii2
/ laravel
来说,queryString
参数也 pathinfo
化,但 路由声明
在一些场景下会很冗余(参数少而固定还好,多且不固定就尴尬了)。
Thinkphp
TP
有 pathinfo
模式的 url
,可以解析和处理 pathinfo
化的 queryString
,使用框架的 url
助手方法生成 pathinfo
风格的链接。
为什么说 TP
对 pathinfo
很推崇呢,官方给出的 url
重写规则就是不携带 queryString
的 try_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
Yii2
的 prettyUrl
并非完全的 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
Laravel
同 yii2
类似,并非 yii2
/ laravel
不能 pathinfo
,而是说 TP
对 pathinfo
亲和力比较高,可以友好的解析和生成 url
,yii2
/ 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 漏洞
简单讲一下 php
的 cgi.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_name
和 pathinfo
分别是
fastcgi_split_path_info ^(.+?\.php)(/.+)$;
SCRIPT_NAME /uploads/index.jpg/hack.php
PATH_INFO /your/site
fastcgi
将 SCRIPT_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
文件就立即返回。这样就可以关闭恶意使用携带 .php
的 url
去触发捕获了。
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;
}
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。