ThinkPHP 5.0.x
目录结构
1 | project 应用部署目录 |
生命周期
这里根据官方文档的说明,一步步跟进调试源码,来梳理TP5.0.x的整个执行流程,并熟悉核心类库的各个核心类。
入口文件
- 通常是
public/index.php
,可以更改 - 常用于定义系统常量以及加载引导文件
引导文件
默认是/thinkphp/start.php
,并且加载同目录下base.php文件,主要完成以下操作
1.加载系统常量、环境常量和变量(.env)
常见的常量有:
2.注册自动加载机制
任何关于类加载的操作,核心处理类为\think\Loader
,该类定义了autoload
方法,并在引导文件时注册该方法为自动类加载机制的处理方法。
常见类库文件的位置及其根命名空间(默认):
根命名空间 | 描述 | 类库目录 |
---|---|---|
think | 系统核心类库 | thinkphp/library/think |
traits | 系统Trait类库 | thinkphp/library/traits |
app | 应用类库 | application |
think | composer第三方类库 | vendor/topthink |
自定义 | 自定义类库 | extend/自定义名 |
3.
路由
路由模式
三种路由模式
- 普通模式(默认)
该模式下关闭路由'url_route_on' => false
,不会解析任何路由规则,而是完全通过默认的PATH_INFO
模式来访问,即
1 | http://serverName/index.php/module/controller/action/param/value/... |
对于参数解析,默认成对解析,也可以改为顺序解析,即每个参数都是值,索引递增,'url_param_type' => 1
- 混合模式
1 | 'url_route_on' => true, |
该模式下,定义路由规则的则按路由规则访问,未定义的则按普通模式访问。
- 强制模式
1 | 'url_route_on' => true, |
该模式默认不开启,每一个访问必须严格根据路由定义进行dispatch,如果为定义相关路由,则直接抛出异常。
路由定义
- 动态注册
1 | think\Route::rule('路由表达式','路由地址','请求类型','路由参数(数组)','变量规则(数组)'); |
默认在应用目录下的route.php文件,如application/route.php,用法
1 | // 注册路由到index模块的News控制器的read操作 |
对应不同请求类型的还有便利方法有
1 | Route::get('new/:id','News/read'); // 定义GET请求路由规则 |
- 路由表达式
以/
为参数分割符,支持一下几种形式
1 | '/' => 'index' // 首页访问路由 |
- 路由参数
数组形式,对路由规则的补充,常见有以下配置
1 | [ |
- 变量规则
数组形式,对路由表达式中定义的变量增加约束规则(正则),比如限制为数字、字符串等,通常是一系列简单正则的组合,如
1 | [ |
- 定义路由配置文件
在我们所说的route.php的最后通过返回数组的方式直接定义路由规则,所有跟配置相关的php形式,都是返回数组(底层实现就是使用include包含并赋值的)
默认情况下,只会加载一个路由配置文件route.php
,如果你需要定义多个路由文件,可以修改route_config_file
配置参数
1 | 'route_config_file' => ['route', 'route1', 'route2'], |
变量规则
- 类方法
1 | Route::pattern(name, pattern) // 定义全局变量,即所有路由的同名name变量都使用该规则,会被局部变量定义覆盖 |
- 规则
1 | 全局变量 |
路由参数
常用的有
参数 | 说明 | 实例 |
---|---|---|
method | 请求类型检测,支持多个请求类型 | 用于Route::any |
ext | URL后缀检测,支持匹配多个后缀 | V5.0.7 版本以上,ext 和deny_ext 参数允许设置为空,分别表示不允许任何后缀以及必须使用后缀访问。 |
deny_ext | URL禁止后缀检测,支持匹配多个后缀 | 同上 |
https | 检测是否https请求 | |
domain | 域名检测 | 支持完整域名检测、子域名检测(部分匹配即可) |
before_behavior | 前置行为(检测) | 支持使用行为对路由进行检测是否匹配,如果行为方法返回false表示当前路由规则无效。 |
after_behavior | 后置行为(执行) | 路由匹配后,对路由的额外处理 |
callback | 自定义检测方法 | 使用自定义函数进行检测,false不通过,有点类似前置行为检测 |
merge_extra_vars | 合并额外参数 | 通常用于完整路由,如果有额外的参数则合并作为变量值,即多余的部分合并为一个整体,如new/:name$ 规则下new/thinkphp/hello ,则name匹配thinkphp/hello |
bind_model | 绑定模型(V5.0.1+ ) |
'bind_model'=>['User','name'] ,当前路由匹配后绑定一个模型,依赖注入获取 |
cache | 请求缓存(V5.0.1+ ) |
'cache'=>3600 ,单位 s |
param_depr | 路由参数分隔符(V5.0.2+ ) |
|
ajax | Ajax检测(V5.0.2+ ) |
|
pjax | Pjax检测(V5.0.2+ ) |
V5.0.7
版本以上,ext
和deny_ext
参数允许设置为空,分别表示不允许任何后缀以及必须使用后缀访问。
路由地址*
这一部分涉及TP5.0.13的RCE,利用captcha路由到方法,绕过module分支,不用开启debug下即可RCE。
1 | \think\Route::get('captcha/[:id]', "\\think\\captcha\\CaptchaController@index"); |
用以下方式访问即可触发RCE
1 | ?s=captcha |
这里比较绕,需要理清楚\
和/
的区别,命名空间用\
,默认PATH_INFO的分隔符用/
类型 | 格式(指的是路由定义中的路由地址格式,不是URL传入格式) | 用法说明 |
---|---|---|
路由到模块/控制器 | ‘[模块/控制器/操作]?额外参数1=值1&额外参数2=值2…’ | 默认用法,不开启路由模式时也按该方式解析。支持额外参数,支持多级控制器,如index/group.blog/read ,相当于访问二级的group/blog 控制器 |
路由到重定向地址 | ‘外部地址’(默认301重定向) 或者 [‘外部地址’,’重定向代码’] | 开头为/ ,则本地重定向;为http:s? 则重定向到外部,有利于网站前移,注意区分开头有无/ ,若无则表示默认的路由方式,若有则表示重定向 |
路由到控制器方法 | ‘@[模块/控制器/]操作’ | 模块/控制器若绑定的话可以省略,该方式不会初始化模块,直接调用操作,相当于调用Loader::action() 方法 |
路由到类的方法 | ‘\完整的命名空间类::静态方法’ 或者 ‘\完整的命名空间类@动态方法’ | 这个是上面方式的拓展,路由到的类方法不局限于模块中的控制器类,而是可以路由到任意类的方法,注意此处用命名空间,如\app\index\service\Blog::read |
路由到闭包函数 | 闭包函数定义(支持参数传入) | 路由地址的位置变成闭包函数,那么直接调用闭包函数而不去寻找控制器。如Route::get(‘hello/:name’,function($name){ return ‘Hello,’.$name; }); |
资源路由
需要在配置文件中用__rest__
来定义资源路由
1 | '__rest__'=>[ |
这个比较难理解的地方是资源路由的嵌套,还是要注意区分rule()方法的几个参数。我理解的资源路由相当于一种标准的资源对象,有点类似于java Bean,其类方法由特定的规则或类型组成(比如规定方法名称必须是什么样)。然后通过URL中特定的访问规则(即路由规则),从而定位到规定资源路由控制器类的特定操作。这里的资源路由类需要由以下方法。
标识 | 请求类型 | 生成路由规则 | 对应操作方法(默认) |
---|---|---|---|
index | GET | blog |
index |
create | GET | blog/create |
create |
save | POST | blog |
save |
read | GET | blog/:id 注意id变量 |
read |
edit | GET | blog/:id/edit |
edit |
update | PUT | blog/:id |
update |
delete | DELETE | blog/:id |
delete |
两个方法
方法1
1 | Route::resource('路由规则','资源路由控制器类',选项) |
比如:
1 | Route::resource('blog','index/blog',['var'=>['blog'=>'blog_id']]); |
访问(GET方法访问):
1 | http://serverName/blog/ //对应index/blog/index |
而进一步对应
1 | public function index() |
方法2
1 | Route::rest('原操作名', ['method', '/更改后的操作名', '更改后的操作名']) |
该方法用于更改默认的资源操作,支持批量更改,如
1 | Route::rest('create',['GET', '/add','add']); |
那么访问
1 | http://serverName/blog/create |
资源嵌套
1 | Route::resource('blog.comment','index/comment'); |
最后指向资源处理控制器还是comment
,所以在__rest__
中注册时要注册comment
的,不同的是路由规则变了,且允许两个变量,一个变量对应blog的,一个变量对应comment的,比如
1 | blog/:blog_id/comment/:id/edit |
前缀的变量名默认为xxx_id
对应的操作
1 | namespace app\index\controller; |
其他特性
其他特性大部分都是为了简化URL的特性。
- 快捷路由
给模块/控制器一个别名,且针对操作可以指定method前缀
1 | Route::controller('user','index/User'); |
定义
1 | class User { |
访问
1 | get http://localhost/user/info |
- 路由别名
快捷路由第一个功能的plus版,且额外支持其他的路由参数
- 路由绑定
三种绑定方式:绑定到模块/控制器/操作(直接访问操作)、绑定到命名空间(访问类/操作)、绑定到类(直接访问操作)
控制器
请求
请求的核心类是\think\Request
类,可以使用以下两种方式生成一个该类对象
1 | $request = Request::instance(); |
获取请求/变量信息*
支持一堆获取请求信息的方式。包括获取$_GET
等外部请求信息,以及路由、模块、控制器等其他信息内置请求信息。有几种类型:
1 | 1.针对$_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 变量 |
这里特别注意一下过滤方法,即第三个参数,其默认配置为
1 | 默认全局过滤方法 |
这里可以看到,过滤方法名就是php内置函数htmlspecialchars
,所以这里可以联想一下,其底层实现有没有可能使用call_user_func
之类的函数调用方法,而TP5的调用任意method的RCE的Sink点确实在filterValue
方法中使用了call_user_func
。所以针对过滤函数、回调方法之类的关键词,可以稍微关注一下其底层实现,从特性定义来思考到其底层实现的一些潜在的危险操作。
更改变量
1 | // 更改GET变量 |
请求类型
使用isXxx
的函数形式,返回bool值
1 | // 是否为 GET 请求 |
请求伪装*
这个也涉及TP5调用Request类方法来RCE。其中我们传入的_method
就是请求伪装变量,其会保存在$this->method
中,从而通过
1 | $this->{$this->method}($_POST); |
导致调用任意Requst类方法方法
有以下几种伪装形式
1 | 1.伪装method: |
伪静态
伪静态即给访问的URL地址添加特定后缀,具有静态页面的URL特征,也有利于优化SEO
设置
1 | 'url_html_suffix' => 'html|...|...' //默认为html |
访问
1 | http://serverName/index/blog/3 |
获取
1 | Request::instance()->ext(); |
几种注入
1.方法注入
即动态自定义Request类的方法
1 | Request::hook('方法调用名','方法定义名'); |
定义
1 | function getUserInfo(Request $request, $userId) //第一个参数必须为Request $xxx |
调用
1 | Request::instance()->user("xxx"); |
2.属性注入
顾名思义,可以为当前Request类注入一些属性
绑定
1 | Request::instance()->bind('user',new User); |
获取
1 | Request::instance()->user; |
3.依赖注入
依赖注入相当于将一个对象通过方法的参数注入进该方法中,从而在该方法中可以获取/调用这个注入对象的所有属性/方法,主要是通过bindParams和getParamValue来完成底层实现,最后通过反射类的invokeArgs方法完成调用,支持的依赖注入方式有
- 自动注入请求
即在操作方法中定义Request $request
,即可自动注入Request类,其底层先通过ReflectionMethod类的getParameters获取被注入的操作方法的参数,包含request,然后获取该参数名后再调用ReflectionMethod类的getClass获取参数的类型提示类,若该类不为空。则先判断该类是否可以通过Request::instance()->$name
的方式直接获取(即之前是否属性注入),若有,则直接返回,若无,则进一步判断该类是否有public static属性的invoke
方法,若有,则直接注入Request
对象,若无则实例化要注入的对象,最后返回。
因此根据底层分析,也就衍生出了一系列功能
1 | 1.架构方法注入 |
- 其他对象注入
其他对象注入支持事先的属性注入,若未绑定,则注入一个默认初始化的对象,相当于
1 | # 事先属性注入 |
同样地
1 | 1.架构方法注入 |
- invoke方法自动注入Request对象
1 | namespace app\index\model; |
参数绑定
支持名称对参数绑定(默认)和顺序参数绑定(简化URL),相当于URL中的变量与操作参数定义的同名变量一一对应,有点类似于函数的名称参数。底层实现还是依靠ReflectionMethod类的getParameters的方法,然后再getName获取名称,如果存在则直接带名称调用invokeArgs
,否则抛出索引不存在异常,其实现模板应该是参考的这个
https://www.php.net/manual/zh/reflectionmethod.invokeargs
1 | We can do black magic, which is useful in templating block calls: |