Fork me on GitHub

ThinkPHP框架学习(含数据库及I函数)

参考文献:https://www.kancloud.cn/manual/thinkphp
https://www.jianshu.com/p/ef3ee8260b2d

www WEB子目录

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
├─index.php       入口文件
├─README.md README文件仅用于说明,实际部署的时候可以删除。
├─Application 应用目录,默认是空的,但是第一次访问入口文件会自动生成
├─Public 资源文件目录
└─ThinkPHP 框架目录
│ ├─Common 核心公共函数目录
│ ├─Conf 核心配置目录
│ ├─Lang 核心语言包目录
│ ├─Library 框架类库目录
│ │ ├─Think 核心Think类库包目录
│ │ ├─Behavior 行为类库目录
│ │ ├─Org Org类库包目录
│ │ ├─Vendor 第三方类库目录
│ │ ├─ ... 更多类库目录
│ ├─Mode 框架应用模式目录
│ ├─Tpl 系统模板目录
│ ├─LICENSE.txt 框架授权协议文件
│ ├─logo.png 框架LOGO文件
│ ├─README.txt 框架README文件
│ └─ThinkPHP.php 框架入口文件

入口文件(index.php)

1
2
3
//定义应用入口文件
define('APP_PATH','./Application/');
require './ThinkPHP/ThinkPHP.php';

APP_PATH的定义支持相对路径和绝对路径,但必须以“/”结束

自动创建目录(第一次访问index.php):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Application
├─Common 应用公共模块(不能直接访问)
│ ├─Common 应用公共函数目录
│ └─Conf 应用公共配置文件目录
├─Home 默认生成的Home模块
│ ├─Conf 模块配置文件目录
│ ├─Common 模块公共函数目录
│ ├─Controller 模块控制器目录
│ ├─Model 模块模型目录
│ └─View 模块视图文件目录
├─Runtime 运行时目录
│ ├─Cache 模版缓存目录
│ ├─Data 数据目录
│ ├─Logs 日志目录
│ └─Temp 缓存目录

在自动生成目录结构的同时,在各个目录下面还生成了index.html文件,这是ThinkPHP自动生成的目录安全文件。为了避免某些服务器开启了目录浏览权限后可以直接在浏览器输入URL地址查看目录,系统默认开启了目录安全文件机制,会在自动生成目录的时候生成空白的index.html文件

控制器
在自动生成的Application/Home/Controller目录下面有一个 IndexController.class.php 文件,这就是默认的Index控制器文件。
控制器类的命名方式是

  • 控制器名(驼峰法,首字母大写)+Controller
  • 控制器文件的命名方式是:类名+class.php(类文件后缀)

命名规范

  • 类文件都是以.class.php为后缀(这里是指的ThinkPHP内部使用的类库文件,不代表外部加载的类库文件),使用驼峰法命名,并且首字母大写,例如 DbMysql.class.php
  • 类的命名空间地址所在的路径地址一致,例如Home\Controller\UserController类所在的路径应该是 Application/Home/Controller/UserController.class.php
  • 确保文件的命名调用大小写一致,是由于在类Unix系统上面,对大小写是敏感的(而ThinkPHP在调试模式下面,即使在Windows平台也会严格检查大小写);
  • 类名文件名一致(包括上面说的大小写一致),例如 UserController类的文件命名是UserController.class.phpInfoModel类的文件名是InfoModel.class.php, 并且不同的类库的类命名有一定的规范;
  • 函数、配置文件等其他类库文件之外的一般是以.php为后缀(第三方引入的不做要求);
  • 函数的命名使用小写字母和下划线的方式,例如 get_client_ip
  • 方法的命名使用驼峰法,并且首字母小写或者使用下划线“_”,例如 getUserName_parseType,通常下划线开头的方法属于私有方法;
  • 属性的命名使用驼峰法,并且首字母小写或者使用下划线“_”,例如 tableName_instance,通常下划线开头的属性属于私有属性;
  • 以双下划线__打头的函数或方法作为魔法方法,例如__call__autoload
  • 常量以大写字母和下划线命名,例如 HAS_ONE和 MANY_TO_MANY
  • 配置参数以大写字母和下划线命名,例如HTML_CACHE_ON
  • 语言变量以大写字母和下划线命名,例如MY_LANG,以下划线打头的语言变量通常用于系统语言变量,例如_CLASS_NOT_EXIST_
  • 变量的命名没有强制的规范,可以根据团队规范来进行;
  • ThinkPHP的模板文件默认是以.html 为后缀(可以通过配置修改);
  • 数据表字段采用小写加下划线方式命名,并注意字段名不要以下划线开头,例如 think_user表和 user_name字段是正确写法,类似 _username 这样的数据表字段可能会被过滤。

配置加载

1
惯例配置->应用配置->模式配置->调试配置->状态配置->模块配置->扩展配置->动态配置//配置的优先顺序从右到左

  • 框架内置有一个惯例配置文件(ThinkPHP/Conf/convention.php
  • 应用配置文件也就是调用所有模块之前都会首先加载的公共配置文件(Application/Common/Conf/config.php
  • 如果使用了普通应用模式之外的应用模式的话,还可以为应用模式单独定义配置文件,文件命名规范是: Application/Common/Conf/config_应用模式名称.php(仅在运行该模式下面才会加载)(可选)
  • 开启调试模式,会自动加载框架的调试配置文件(ThinkPHP/Conf/debug.php)和应用调试配置文件(Application/Common/Conf/debug.php)(可选)
  • 在公司和家里分别设置不同的数据库测试环境。在公司,在入口文件中定义:define('APP_STATUS','office');就会自动加载该状态对应的配置文件(Application/Common/Conf/office.php)。回家后,修改为:define('APP_STATUS','home');就会自动加载该状态对应的配置文件(Application/Common/Conf/home.php)。(可选)

读取配置
配置文件,统一使用系统提供的C方法来读取已有的配置。(ThinkPHP/Common/functions.php)
用法:C('参数名称')

1
2
$model = C('URL_MODEL');//读取当前的URL模式配置参数
C('my_config',null,'default_config');//如果my_config尚未设置的话,则返回default_config字符串

因为配置参数是全局有效的,因此C方法可以在任何地方读取任何配置,即使某个设置参数已经生效过期了

加载扩展配置
①在Application/Home/Conf目录下新建user.php,内容如下

1
2
3
4
5
6
<?php
return array(
'USER_TYPE' => 2, //用户类型
'USER_AUTH_ID' => 10, //用户认证ID
'USER_AUTH_TYPE' => 2, //用户认证模式
);//可以不用加?>

②修改同级的config.php

1
2
3
4
<?php
return array(
'LOAD_EXT_CONFIG'=> array('USER'=>'user')
);

③修改Application/Home/Controller/IndexController.class.php

1
2
3
4
5
6
7
8
<?php
namespace Home\Controller;
use Think\Controller;
class IndexController extends Controller {
public function index(){
print_r(C('USER'));
}
}

打开localhost,会出现Array ( [USER_TYPE] => 2 [USER_AUTH_ID] => 10 [USER_AUTH_TYPE] => 2 )

数据库操作分析
①系统公共函数库: \ThinkPHP\Common\functions.php(封装了TP开放给外部的函数)
②ThinkPHP Model模型类: ThinkPHP\Library\Think\Model.class.php (TP的数据库架构类,提供curd类库,是一个对外的接口 )
③TP内部curd类: ThinkPHP\Library\Think\Db\Driver.class.php (这个类的函数都被Model类中的curd操作间接的调用)

Tp在执行数据库操作之前

  • 函数M使用了以后会自动创建new Model类并且会实例化为一个对象返回此资源。
  • 接着这个对象调用了where方法并且格式化处理以后,会将这个值赋值给此对象的一个成员变量$options(注:如果说我们在此对象中有调用其他的方法赋值例如where,table,alias,data,field,order,limit,page,group,having,join,union,distinct,lock,cache,comment等等这种操作方法,那么都会先赋值给此对象,而不是在代码直接进行sql语句拼接,所以我们使用Tp的连贯操作的时候,就不需要像SQL语句拼接那样需要考虑到关键字的顺序问题
  • 处理完了前面选项之后,接下来就会去调用我们的find()方法去调用底层的一个select方法(Driver.class.php这个类中的select方法)来获取数据。所谓的find()方法就是等同于先给此对象的一个成员变量$options赋值操作limit=1然后进行select操作来获取对应的数据。
    最终的sql语句:SELECT * FROMblog_adminWHERE username='admin' limit 1
    如果给赋值了一个操作:
    M('admin')->field('username,password')->where( array('username'=>$username) )->find();
    执行的语句为SELECTusername,password FROMblog_adminWHERE username='admin' limit 1

thinkphp\ThinkPHP\Library\Think\Model.class.php 里的重要成员变量:

1
2
3
4
protected $pk         =   'id';// 主键名称
protected $fields = array(); // 字段信息
protected $options = array(); // 数据信息
protected $methods = array('strict','order','alias','having','group','lock','distinct','auto','filter','validate','result','token','index','force');// 链操作方法列表

where()方法的执行过程:

1
$parse = array_map(array($this->db,'escapeString'),$parse);//对传递到字符串类型的数据,调用mysql_escape_string函数来处理任何返回

可以通过官方文档查看TP如何防止SQL注入:(http://document.thinkphp.cn/manual_3_2.html#sql_injection)
where() 方法如果 传递的是$Model->where("id=%d and username='%s' and xx='%f'",array($id,$username,$xx)) 这种格式的,就会进行 mysql 的mysql_escape_string函数进行处理(mysql_escape_string总是将“ ‘ ”转换成“ \ ”)。 处理完成以后就会将已处理完成的数组赋值到M对象的成员函数$this->options['where'](options为success,error,display…..) 随后返回,供我们进行下一步的处理。

find()方法的执行过程:

1
2
$options     =   $this->_parseOptions($options);//分析表达式,获取执行的表的名称,获取模型的名称,最后对表的字段进行处理。
$resultSet = $this->db->select($options);//调用db对象里的select方法查询数据

此方法的功能就是 获取主键,完善model类的成员变量,options数组,然后实例化 db类,调用select 方法获取数据,然后处理数据完以后返回数据

Find方法使用的 $this->_parseOptions()
这个方法的主要就是 获取操作的表名,查看是否有取别名,获取操作的模型,比对当前表的数据库字段是否一致,若有不一致的字段 $this->options[‘strict’] 设置了的时侯,进行报错处理 否则进行删除多余字段的处理。 执行过滤的方法为_parseType ,功能是数据类型检测并且进行强制转换(强制转换的类型为int,float,bool 三种类型)

Find方法使用的$this->db->select() 方法
$this->db 是在ThinkPHP\Library\Think\Db类中的方法
①其中parseSql()这个函数的主要功能是拼接sql语句
$this->parseWhere(!empty($options['where'])?$options['where']:''),
parseWhere 方法比其他的要复杂的多,其他的都是拼接字符串,过滤,然后返回。)

②parseWhere()方法
这方法会去判断传进来的变量内容是否是字符串,如果是的话,就直接返回,如果不是字符串而是数组的话,那么就会挨个的解析,并且判断是否是特殊的条件表达式,如果是,调用parseThinkWhere()方法(主要是解析特殊的条件并且调用parseValue ()方法),已上条件都不匹配的情况下就认为是普通查询 普通查询都会调用parseWhereItem 方法

③parseWhereItem()方法

1
2
3
4
5
6
7
8
9
10
11
 protected function parseWhereItem($key,$val)//$val值是从where函数接收到的数据,然后经过这个方法会先去判断是否是数组,不是数组就会到下面的流程
......
$exp = strtolower($val[0]);//$exp的值=执行表达式(可以操作sql拼接的流程)=$val[0]
······
elseif('bind' == $exp ){ // 使用表达式
$whereStr .= $key.' = :'.$val[1];
}elseif('exp' == $exp ){ // 使用表达式
$whereStr .= $key.' '.$val[1];
}elseif(preg_match('/^(notin|not in|in)$/',$exp)){ // IN 运算
if(isset($val[2]) && 'exp'==$val[2]) {
$whereStr .= $key.' '.$this->exp[$exp].' '.$val[1];//这三句 $whereStr .没有使用TP的过滤。

进入此方法以后会发现这个方法会根据 $exp 变量的不同 拼接不同的sql语句 而在这个方法中看到最多的就是parseValue()方法了

④parseValue ()方法
这个方法会去调用escapeString()方法 (将传进来的变量进行addslashes 然后返回)

嗯。。。。。。。整体流程看起来我们可以发现TP使用的过滤方法就是一个简单addslashes 来防止过滤

I函数(官方http://document.thinkphp.cn/manual_3_2.html#input_filter)
路径:ThinkPHP\Common\functions.php
方法名:function I($name,$default='',$filter=null,$datas=null)
主要功能

  • 确定数据类型
  • 对数据进行循环取值
  • 调用think_filter 函数进行过滤
    在think_filter中:
    1
    2
    if(preg_match('/^(EXP|NEQ|GT|EGT|LT|ELT|OR|XOR|LIKE|NOTLIKE|NOT BETWEEN|NOTBETWEEN|BETWEEN|NOTIN|NOT IN|IN)$/i',$value)){
    $value .= ' ';//正则匹配,过滤查询特殊字符,然后在这些条件表达式后面添加一个空格

例如:

1
2
3
4
5
没有使用think_filter 函数时:
goods_name[0]=in&goods_name[1]=(true) and (updatexml(1,concat(1,(select user())),1))--&goods_name[2]=exp

使用了think_filter函数时:
goods_name[0]=in &goods_name[1]=(true) and (updatexml(1,concat(1,(select user())),1))--&goods_name[2]=exp

其中elseif(preg_match('/^(notin|not in|in)$/',$exp))If( in(空格) == 'in')不匹配,也就防止了sql注入的产生

审计方式
①进入目录直接搜索 $_POST $_GET 查看是否带入了 where 查询。
② 全局搜索 where 查看是否有字符串拼接的痕迹
③全局搜索 order,having,group,alias (例:当我们可以操控 order传进去的参数时,也是可以进行注入的,这是因为tp对order方法只是一个字符串拼接的操作 )
④全局搜索 join ,field,执行的都是字符串拼接,只要可以外部操作就可以产生注入

-------------本文结束感谢您的阅读-------------