说明
该文章来源于同事lu2ker转载至此处,更多文章可参考:https://github.com/lu2ker/
TP5框架简单理解
(PS:只做粗略、关键知识的记录,TP程序的开始。详情请阅读官方手册)
1. 架构总览
TP程序的开始
PHP >=5.3.0, PHP7
ThinkPHP5.0
应用基于MVC
(模型-视图-控制器)的方式来组织。MVC是一个设计模式,它强制性的使应用程序的输入、处理和输出分开。使用MVC应用程序被分成三个核心部件:模型(M)、视图(V)、控制器(C),它们各自处理自己的任务。
5.0的URL访问受路由决定,如果关闭路由或者没有匹配路由的情况下,则是基于:
http://serverName/index.php/模块/控制器/操作/参数/值…
1.1 控制器/操作
5.0的控制器类比较灵活,可以无需继承任何基础类库。控制器不应该过多的介入业务逻辑处理
一个典型的Index
控制器类如下:控制器就是一个类,类里的不同的方法就是不同的操作
namespace app\index\controller;
use think\Request;
class Index
{
public function index()
{
return 'hello,thinkphp!';
}
public function test(){
$name = Request::instance()->param('name');
echo "My name is " . $name;
}
}
操作方法可以不使用任何参数,如果定义了一个非可选参数,则该参数必须通过用户请求传入,如果是URL请求,则通常是$_GET
或者$_POST
方式传入。
- 如上代码第一行是确定了命名空间。什么是命名空间?
在PHP中,命名空间用来解决在编写类库或应用程序时创建可重用的代码如类或函数时碰到的两类问题。
- 用户编写的代码与PHP内部的类/函数/常量或第三方类/函数/常量之间的名字冲突
- 为很长的标识符名称(通常是为了缓解第一类问题而定义的)创建一个别名(或简短)的名称,提高源代码的可读性。
而TP5的思想是
ThinkPHP5
采用命名空间方式定义和自动加载类库文件,有效的解决了多模块和Composer
类库之间的命名空间冲突问题,并且实现了更加高效的类库自动加载机制。
- 如上代码第二行use就是加载类库,调用TP5写好的Request类,其中实现了一些请求方法。
相对应的就是上面代码11行的Request::instance()->param('name');
即获取客户端传入的参数name
1.2 MVC模式流程
通常一个基于TP开发的CMS,可能有很多个模块,而每个模块都是控制器—模型—视图完整的。控制器根据要求选择调用模型,模型进行业务逻辑处理,交给视图渲染。这个流程。
一个模块下面有多个
控制器
负责响应请求,而每个控制器
其实就是一个独立的控制器类
。控制器
主要负责请求的接收,并调用相关的模型
处理,并最终通过视图
输出。严格来说,控制器
不应该过多的介入业务逻辑处理。
模型类
通常完成实际的业务逻辑和数据封装,并返回和格式无关的数据
。控制器调用模型类后返回的
数据
通过视图
组装成不同格式的输出。视图
根据不同的需求,来决定调用模板引擎
进行内容解析后输出还是直接输出。
1.3 类库自动加载
命名空间的路径与类库文件的目录一致,那么就可以实现类的自动加载
。
如果想调用不同命名空间下的类库的方法,就要先用use
给加载上。
1.4 URL访问检测
应用初始化完成后,就会进行URL的访问检测,包括PATH_INFO
检测和URL后缀检测。
5.0的URL访问必须是PATH_INFO
方式(包括兼容方式)的URL地址,例如:
http://serverName/index.php/index/index/hello/val/value
所以,如果你的环境只能支持普通方式的URL参数访问,那么必须使用
http://serverName/index.php?s=/index/index/hello&val=value
但是,这样也是行的:
http://serverName/index.php/index/index/hello/?val=value
我的环境可以用上面三种方式,都是默认配置。
1.5 路由模式
1.5.1 普通模式
‘url_route_on’ => false,
关闭路由,使用默认的PATH_INFO模式访问URL:如上面 1.4URL访问检测 的第一个
http://serverName/index.php/module/controller/action/param/value/…
1.5.2 混合模式
‘url_route_on’ => true,
‘url_route_must’=> false,
开启路由,也就是允许了自定义路由,同时对没有定义的路由使用默认的普通模式
1.5.4 强制路由
‘url_route_on’ => true,
‘url_route_must’=> true,
这种方式下面必须严格给每一个访问地址定义路由规则(包括首页),否则将抛出异常。
首页的路由规则采用/
定义即可,例如下面把网站首页路由输出Hello,world!
Route::get('/',function(){
return 'Hello,world!';
});
1.6 路由定义
ThinkPHP5 中支持 5种 路由地址方式定义:
定义方式 | 定义格式 |
---|---|
方式1:路由到模块/控制器 | ‘[模块/控制器/操作]?额外参数1=值1&额外参数2=值2…’ |
方式2:路由到重定向地址 | ‘外部地址’(默认301重定向) 或者 [‘外部地址’,‘重定向代码’] |
方式3:路由到控制器的方法 | ‘@[模块/控制器/]操作’ |
方式4:路由到类的方法 | ‘\完整的命名空间类::静态方法’ 或者 ‘\完整的命名空间类@动态方法’ |
方式5:路由到闭包函数 | 闭包函数定义(支持参数传入) |
1.6.1方式 1:路由到模块/控制器
// 路由到默认或者绑定模块
'blog/:id'=>'blog/read', # 意思是如果访问blog/5,就会调用blog/read并传入参数id=5,下面同理
// 路由到index模块
'blog/:id'=>'index/blog/read',
----------------------------------------------
namespace app\index\controller;
class Blog {
public function read($id){
return 'read:'.$id;
}
}
还支持多级控制器:
//多级控制器方式
'blog/:id'=>'index/group.blog/read'
----------------------------------------------
namespace app\index\controller\group;
class Blog {
public function read($id){
return 'read:'.$id;
}
}
1.6.2 方式2:路由到重定向地址
举个例子,如果我们希望avatar/123
重定向到/member/avatar/id/123_small的话,只能使用:
'avatar/:id'=>'/member/avatar/id/:id_small'
# 区别于 'avatar/:id'=>'avatar/read' ,这个不是采用301重定向的。重定向的外部地址必须以“/”或者http开头的地址。
路由地址采用重定向地址的话,如果要引用动态变量,直接使用动态变量即可。
采用重定向到外部地址通常对网站改版后的URL迁移过程非常有用,例如:
'blog/:id'=>'http://blog.thinkphp.cn/read/:id'
表示当前网站(可能是http://thinkphp.cn )的 blog/123地址会直接重定向到 http://blog.thinkphp.cn/read/123。
1.6.3 方式3:路由到控制器的方法
'blog/:id'=>'@index/blog/read',
-------------------------------------
会执行 Loader::action('index/blog/read');
相当于直接调用 \app\index\controller\blog类的read方法。
namespace app\index\controller;
class Blog {
public function read($id){
return 'read:'.$id;
}
}
1.6.4 方式4:路由到类的方法
'blog/:id'=>'\app\index\service\Blog@read'(动态方法)
或
'blog/:id'=>'\app\index\service\Blog::read',(静态方法)
执行的是 \app\index\service\Blog类的read方法。
1.7 路由使用
application目录是这个应用的目录,index目录是这个应用的一个模块,其内的controller目录存放的是这个模块的控制器,同理一般modle目录就存放的模型,view目录就放的视图。(这里是单纯的框架所以没有)
#index.php
<?php
namespace app\index\controller;
use think\Request;
class Index
{
public function index()
{
echo "hahaha";
}
public function test(){
$name = Request::instance()->param('name');
echo "My name is " . $name;
}
}
1.8 路由的其他知识
// 使用注解路由
'route_annotation' => true,
class Index
{
/**
* @param string $name 数据名称
* @return mixed
* @route('hello/:name','get')
*/
public function hello($name)
{
return 'hello,'.$name;
}
}
其余的直接看这里(TP5.1的)就好
1.9 其他东西
tarit
关键字关于这个东西,看PHP手册就行,我的目录下有CHM格式的手册;
'default_return_type'=>'json'
修改config.php下的这个配置,可以使输出自动进行数据转换处理。Response
类会统一处理。
2.请求相关
2.1 如何获取请求参数的?
首先要use一下TP写好的Request类,然后调用的话可以用很多种写法,下面是一种
<?php
namespace app\index\controller;
use think\Request;
class Index // class Index extends Controller 解释如下:
//如果你继承了系统的控制器基类think\Controller的话,系统已经自动完成了请求对象的构造方法注入了,你可以直接使用$this->request属性调用当前的请求对象。
{
/**
* @var \think\Request Request实例
*/
protected $request;
/**
* 构造方法
* @param Request $request Request对象
* @access public
*/
public function __construct(Request $request)
{
$this->request = $request;
}
public function index()
{
return $this->request->param('name');
}
}
还可以这些样子:
Request::instance=>param();//获取所有参数[ 结果类型数组],不分请求类型;
Request::instance=>param('name');//获取单个参数[即:直接填写变量名即可];
Request::instance=>get();//获取?后面的参数;
Request::instance=>route();//获取路由里面的参数;
Request::instance=>post();//获取post请求参数
eg:
public function hello()
{
$res=Request::instance()->param();
var_dump($res);
}
//这是依赖注入方式,无论是否继承系统的控制器基类,都可以使用操作方法注入。
public function hello(Request $request)
{
$res=$request->param();
var_dump($res);
}
//也可以使用助手函数
$request = request();
2.2 检测变量是否设置
可以使用has
方法来检测一个变量参数是否设置,如下:
Request::instance()->has('id','get');
Request::instance()->has('name','post');
或者使用助手函数(助手函数很棒很简单)
input('?get.id');
input('?post.name');
2.3 请求方法
可以通过Request
对象完成全局输入变量的检测、获取和安全过滤,支持包括$_GET
、$_POST
、$_REQUEST
、$_SERVER
、$_SESSION
、$_COOKIE
、$_ENV
等系统变量,以及文件上传信息。
方法 | 描述 |
---|---|
param | 获取当前请求的变量 |
get | 获取 $_GET 变量 |
post | 获取 $_POST 变量 |
put | 获取 PUT 变量 |
delete | 获取 DELETE 变量 |
session | 获取 $_SESSION 变量 |
cookie | 获取 $_COOKIE 变量 |
request | 获取 $_REQUEST 变量 |
server | 获取 $_SERVER 变量 |
env | 获取 $_ENV 变量 |
route | 获取 路由(包括PATHINFO) 变量 |
file | 获取 $_FILES 变量 |
也可以在获取变量的时候添加过滤方法,例如:
Request::instance()->get('name','','htmlspecialchars'); // 获取get变量 并用htmlspecialchars函数过滤
Request::instance()->param('username','','strip_tags'); // 获取param变量 并用strip_tags函数过滤
Request::instance()->post('name','','org\Filter::safeHtml'); // 获取post变量 并用org\Filter类的safeHtml方法过滤
//可以支持传入多个过滤规则,例如:
Request::instance()->param('username','','strip_tags,strtolower');// 获取param变量 并依次调用strip_tags、strtolower函数过滤
2.4 【▲】请求方法伪装
支持请求类型伪装,可以在POST
表单里面提交_method
变量,传入需要伪装的请求类型,例如:
<form method="post" action="">
<input type="text" name="name" value="Hello">
<input type="hidden" name="_method" value="PUT" >
<input type="submit" value="提交">
</form>
提交后的请求类型会被系统识别为PUT
请求。你可以设置为任何合法的请求类型,包括GET
、POST
、PUT
和DELETE
等。如果你需要改变伪装请求的变量名,可以修改应用配置文件:
// 表单请求类型伪装变量
'var_method' => '_m',
2.5 伪静态配置效果
'url_html_suffix' => 'shtml'
http://serverName/Home/Blog/read/id/1
等价于
http://serverName/Home/Blog/read/id/1.shtml
如果'url_html_suffix' => ''
则
任何后缀都能正常访问
// 关闭伪静态后缀访问
'url_html_suffix' => false,
http://serverName/index/blog/read/id/3.html
id参数的值会被解析为3.html
2.6 参数绑定
就是给控制器下的方法(函数),给上参数,这样在url访问的时候就直接加上这个参数再给个值就能自动获取,不需要写Request。
2.7 请求缓存
例子:如果设置了这样一条路由,其中指定了cache字段。
Route::get('bind/:id','Index/bind',['cache'=>3600]);
那么只有第一次访问时会走正常的C-M-V流程,也就是会真正去调用控制器下的操作方法。
之后再访问同样路由的话,检测到同样的路由就不会再去调用控制器下的操作方法了,而是直接从缓存中获取响应。很给力。
3. 数据库
相信跟完那几个SQLi漏洞的代码分析,就已经对TP种数据库操作有些了解了,故略。
4. 模板
4.1 变量输出
// index.php 控制器
use think\Controller;
use think\View;
class Index extends Controller
{
public function index()
{
$view = new View();
$view->name = 'thinkphp';
return $view->fetch();
}
}
# index.html
<html>
Hello,{$name}!
</html>
可以大致理解为:访问index方法,fetch渲染时会把name变量给传到index.html,而模板引擎解析的{$name}
实际上就是<?php echo($name);?>
注意模板标签的{
和$
之间不能有任何的空格,否则标签无效。
输出数组变量的value用{$array.key}
输出对象属性value用{$obj:property}
或者${obj->property}
输出其他变量(比如系统变量)以KaTeX parse error: Expected '}', got 'EOF' at end of input: Think开头,比如`{Think.cookie.name}`
4.2 使用函数、默认值
{$name|md5|strtoupper|substr=0,3}
# 解释:多个函数之间用“|”分割
编译后即为:
<?php echo (substr(strtoupper(md5($name)),0,3)); ?>
# 还可以简单的写法
{:substr(strtoupper(md5($name)),0,3)}
# 加个默认值
{$user.nickname|default="这家伙很懒,什么也没留下"}
4.3 TP内置标签
内置标签还是全一点把,遇到忘了的直接去开发手册查
PS:这儿有一个简单的MVC代码案例
5. 日志和错误
6. 杂项
6.1 缓存
支持的缓存类型包括file、memcache、wincache、sqlite、redis和xcache。
驱动方式就是指什么形式存 缓存的数据
如果定义了多个缓存驱动:
// 切换到file操作
Cache::store('file')->set('name','value');
Cache::get('name');
// 切换到redis操作
Cache::store('redis')->set('name','value');
Cache::get('name');
在TP类库里面thinkphp\library\think\db\Query.php这个数据库查询类调用Cache比较多。
Session 、Cookie 和 缓存差不多。
6.2 上传规则
默认情况下,会在上传目录下面生成以当前日期为子目录,以微秒时间的md5
编码为文件名的文件。
/upload/20160510/42a79759f284b767dfcb2a0197904287.jpg
而且一般都在public/upload/目录下。
还有可能以32位哈希的前两位当子目录。
上传时可以用 f i l e − > r u l e ( ) 自定义文件名生成的规则,如 ‘ file->rule()自定义文件名生成的规则,如` file−>rule()自定义文件名生成的规则,如‘file->rule(‘md5’)->move(‘/home/www/upload/’);`
File类的成员函数move()执行成功后文件成功上传,返回一个File类的对象,失败返回false。可以看看File类都有哪些成员,毕竟是文件相关的类,很有用。
File
类继承了PHP的SplFileObject
类,因此可以调用SplFileObject
类所有的属性和方法。SplFileObject手册解释
Controller
大多是一些模板渲染的方法。从构造函数中就可以看出来,
public function __construct(Request $request = null)
{
$this->view = View::instance(Config::get('template'), Config::get('view_replace_str'));
$this->request = is_null($request) ? Request::instance() : $request;
// 控制器初始化
$this->_initialize();
// 前置操作方法
if ($this->beforeActionList) {
foreach ($this->beforeActionList as $method => $options) {
is_numeric($method) ?
$this->beforeAction($options) :
$this->beforeAction($method, $options);
}
}
}
刚开始就实例化了View类。想到之前复现的ThinkPHP的SQL注入漏洞,有些测试代码就会调用fetch、assign、display这些方法。而模块开发的开始就是从 application\模块\controller 下面开始的。应用模块的控制器如果继承了Controller类,就能直接调用这些方法进行视图渲染。
这个类还有两个方法:beforeAction和validate
beforeAction是前置操作:设置 beforeActionList
属性可以指定某个方法为其他方法的前置操作,数组键名为需要调用的前置方法名,无值的话为当前控制器下所有方法的前置方法。
['except' => '方法名,方法名']
#表示这些方法不使用前置方法,
['only' => '方法名,方法名']
#表示只有这些方法使用前置方法。
# demo
protected $beforeActionList = [
'first',
'second' => ['except'=>'hello'],
'three' => ['only'=>'hello,data'],
];
# 类内所有方法调用前都会调用first方法
# 类内hello方法不会调用second这个前置方法
# 只有hello,data方法会前置调用three方法。
如果你在自己继承了Controller的控制器代码中定义了protected $beforeActionList
,那么它就会在构造函数中foreach处理后,调用beforeAction进行only还是except的判断,最后用call_user_func调用。
call_user_func([$this, $method])
这种写法是对类内方法的调用,这里的$this指向的是继承Controller类的Index类。也就是说回调的函数必须在Index类中定义好了。
validate验证器方法主要还是调用的Validate类,应该是为了方便控制器对数据的验证。
具体流程就是用 加载验证器 的 加载器,来加载验证器。。。。。
如果传入的KaTeX parse error: Undefined control sequence: \library at position 25: …个数组,就先用thinkphp\̲l̲i̲b̲r̲a̲r̲y̲\think\Validate…this里面。不是数组的话就直接调用。
Request(5.0.18版本)
前置PHP知识:PHP中self :: 和 this-> 的用法
前置PHP知识:new static 和 new self的区别 -主要就是有继承的时候的区别
下面只列举部分重要方法
初始化和构造函数:instance和__construct
public static function instance($options = [])
{
if (is_null(self::$instance)) {
self::$instance = new static($options);
}
return self::$instance;
}
protected function __construct($options = [])
{
foreach ($options as $name => $item) {
if (property_exists($this, $name)) { //类属性覆盖
$this->$name = $item;
}
}
if (is_null($this->filter)) {
$this->filter = Config::get('default_filter'); //在应用目录下的config.php
}
// 保存 php://input
$this->input = file_get_contents('php://input');
}
instance就是Request的初始化方法,编程中这种是叫“单例模式”,目的是为了防止实例化的时候构造方法被多次调用,就先new了一下自己然后返回。
property_exists判断 t h i s ,也就是本类对象是否存在传入的 this,也就是本类对象是否存在传入的 this,也就是本类对象是否存在传入的name属性,存在即赋值。接着还定义了过滤器,并获取了输入流(请求body)
这样初始化调用:$req = Request::instance()->get('a');
方法注入:hook和__call
public static function hook($method, $callback = null)
{
if (is_array($method)) {
self::$hook = array_merge(self::$hook, $method);
} else {
self::$hook[$method] = $callback;
}
}
public function __call($method, $args)
{
if (array_key_exists($method, self::$hook)) {
array_unshift($args, $this);
return call_user_func_array(self::$hook[$method], $args);
} else {
throw new Exception('method not exists:' . __CLASS__ . '->' . $method);
}
}
hook方法和__call
魔术方法配合,使得可以在类外面自定义Request的方法。算是扩展性的实现。hook主要是来获取自定义函数的,然后因为这个函数本身不在Request中,所以会调用__call
方法,检查这个函数是不是在hook数组里面,在的话就说明这是一个要注入的方法,就将Request类本身的$this对象作为自定义函数的参数传入,通过call_user_func_array进行调用。自定义函数使用闭包的写法:
# application\index\controller\Index.php
public function index()
{
$req = Request::instance();
$callback = function($RR){
echo "匿名函数";
$RR->test111();
};
Request::hook('invoke',$callback);
$req->invoke();
}
# thinkphp\library\think\Request.php添加一个如下方法:
public function test111()
{
echo "可以调用Request本身的方法";
}
# 访问index.php/index/输出:匿名函数可以调用Request本身的方法
当前的请求类型:method
public function method($method = false)
{
if (true === $method) {
// 获取原始请求类型
return IS_CLI ? 'GET' : (isset($this->server['REQUEST_METHOD']) ? $this->server['REQUEST_METHOD'] : $_SERVER['REQUEST_METHOD']);
} elseif (!$this->method) {
if (isset($_POST[Config::get('var_method')])) {
$this->method = strtoupper($_POST[Config::get('var_method')]);
$this->{$this->method}($_POST);
} elseif (isset($_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE'])) {
$this->method = strtoupper($_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE']);
} else {
$this->method = IS_CLI ? 'GET' : (isset($this->server['REQUEST_METHOD']) ? $this->server['REQUEST_METHOD'] : $_SERVER['REQUEST_METHOD']);
}
}
return $this->method;
}
IS_CLI是用来判断是使用命令行还是浏览器执行的,cli代表命令行。
t h i s − > s e r v e r 和 this->server和 this−>server和_SERVER的区别应该是:前者提供了程序可定义的请求方法(在create方法中),后者是获取客户端请求的 请求方法。
如果调用method时没传入true,分三种情况:
① 如果配置了var_method,从$_POST[Config::get('var_method')]
,即$_POST['_method']
获取请求方法。
有意思的是这条语句$this->{$this->method}($_POST);
,用花括号包裹 先执行取值在以
t
h
i
s
调用传入
this调用传入
this调用传入_POST。
② 通过请求包的HTTP头字段X-HTTP-METHOD-OVERRIDE
来设置method
③ 和true === $method时一样
获取当前请求的参数:param
该方法主体中是对请求参数的处理:包括针对不同的请求方法,选择调用Request类里不同的处理方法;还有获取文件上传信息的处理。获取post、get、put、file等请求参数的合并数据。但这些处理都不是方法内实现的,而是调用的Request类的其他成员方法。
获取GET数据:get
支持传入数组:
t h i s − > g e t = a r r a y m e r g e ( this->get = array_merge( this−>get=arraymerge(this->get, $name);
获取POST数据:post
public function post($name = '', $default = null, $filter = '')
{
if (empty($this->post)) {
$content = $this->input;
if (empty($_POST) && false !== strpos($this->contentType(), 'application/json')) {
$this->post = (array) json_decode($content, true);
} else {
$this->post = $_POST;
}
}
if (is_array($name)) {
$this->param = [];
return $this->post = array_merge($this->post, $name);
}
return $this->input($this->post, $name, $default, $filter);
}
比get特别的地方在于,会从php://input输入流中获取数据,这里的$content = $this->input;
是在构造函数中初始化的。然后还能用json_decode处理json数据并返回为数组格式的。
获取cookie参数:cookie
会调用filterValue方法过滤数据。而且如果cookie是个数组的话还会使用array_walk_recursive递归调用filterValue
获取上传的文件信息:file
会实例化File类,@return null|array|\think\File 返回这三种类型的数据。具体获取文件的什么信息再看。
被多处调用的方法:input
该方法有一个参数是$filter默认为空,说明支持可设置的过滤器。
处理流程在ThinkPHP5之SQLI审计分析(一)里面看过了。
递归过滤方法:filterValue
private function filterValue(&$value, $key, $filters)
{
$default = array_pop($filters);
foreach ($filters as $filter) {
if (is_callable($filter)) {
// 调用函数或者方法过滤
$value = call_user_func($filter, $value);
} elseif (is_scalar($value)) {
if (false !== strpos($filter, '/')) {
// 正则过滤
if (!preg_match($filter, $value)) {
// 匹配不成功返回默认值
$value = $default;
break;
}
} elseif (!empty($filter)) {
// filter函数不存在时, 则使用filter_var进行过滤
// filter为非整形值时, 调用filter_id取得过滤id
$value = filter_var($value, is_int($filter) ? $filter : filter_id($filter));
if (false === $value) {
$value = $default;
break;
}
}
}
}
return $this->filterExp($value);
}
$filters是数组类型的,说明可以进行多个过滤器的处理。在判断了过滤器可以被调用后,就马上用call_user_func来调用过滤器处理数据了。
方法开头会用array_pop弹出默认过滤器,是在getFilter方法将所有接收到的过滤器放到数组里后,又在数组末尾加的那个$default
-
利用method的任意方法调用,调用构造函数
__construct
,且调用时会传入$_POST
数据,那么组合起来就是执行method方法可以控制类的成员变量的值。 -
filterValue中存在
call_user_func
函数可以恶意利用,其参数$filters -
getFilter
方法存在这样$filter = $filter ?: $this->filter;
一条语句获取类成员变量filter(由1知可控)。 -
input
方法中存在:先调用getFilter
获取到filter,再使用array_walk_recursive
递归调用filterValue
并传入filter的情况。
综上,如果找到一处先调用method
再调用input
且的调用点,就能够利用call_user_func
造成RCE。
param方法就完美符合条件。