thinkphp3.23的auth详细源码解读(带实例)
thinkphp的auth权限认证对于做网站来讲是非常常用的一个功能,所以特别写一篇文章来帮助自己更好的记忆,毕竟好记性不如烂笔头!
先来看看auth所需要的三个表:
think_auth_group //用户组表
表内的数据:
id为用户组id
title为用户组名称
status为用户组状态,0为禁用,1为启用
rules为用户组启用的权限规则id
think_auth_group_access //用户组明细表
表内的数据:
uid对应think_user表里的用户id
group_id对应think_auth_group里的用户组id
think_auth_rule //认证规则表
表内的数据:
id:规则id
name:规则唯一标识
title:规则中文名称
status:状态,0为禁用,1为启用
type:是否用condition进行验证,1为默认验证,0为不用condition验证
condition:验证条件,空为存在就验证,不为空则按照此内容进行验证
think_user //用户表
表内的数据:
id为用户id
username为用户名
pass为密码
score为积分
这里由于只是示例,所以设置非常简单,密码也是演示一下,具体设置可以根据自己来,而且如果验证的条件不想放入用户表中,可以另外新建一个表,确定有用户id即可,然后配置的时候使用这个表。
上面是auth的数据表结构,虽然代码很清楚,但可能不太直观,为了更清晰的了解auth的数据表结构情况,下面详细绘制了对应的er图,有图就要更好理解些:
这样基本就能看出验证权限的过程,每个表通过同颜色的属性进行关联以此进行相应的查询,以此进行权限验证!
下面我们再来分析thinkphp的auth类的源码:
Auth类里第一部分是设置受保护的$_config,里面存储的是数组形式的默认auth配置
//默认配置,如果用户没有在config文件中配置auth的相关配置,将会采用以下默认的配置 protected $_config = array( "AUTH_ON" => true, // 认证开关 "AUTH_TYPE" => 1, // 认证方式,1为实时认证;2为登录认证。 "AUTH_GROUP" => "auth_group", // 用户组数据表名 "AUTH_GROUP_ACCESS" => "auth_group_access", // 用户-用户组关系表 "AUTH_RULE" => "auth_rule", // 权限规则表 "AUTH_USER" => "member" // 用户信息表
其后是构造函数,构造函数中将上面设置的默认配置变量数组里的auth各个表加上表前缀,然后检查用户有没有在config文件里设置这些值,有的话,就用array_merge覆盖之前的默认配置。
public function __construct() { $prefix = C("DB_PREFIX"); //获取数据库表前缀 /*将表前缀连接上各自的配置表*/ $this->_config["AUTH_GROUP"] = $prefix.$this->_config["AUTH_GROUP"]; $this->_config["AUTH_RULE"] = $prefix.$this->_config["AUTH_RULE"]; $this->_config["AUTH_USER"] = $prefix.$this->_config["AUTH_USER"]; $this->_config["AUTH_GROUP_ACCESS"] = $prefix.$this->_config["AUTH_GROUP_ACCESS"]; /*检查用户有没有配置auth的配置,如果配置了,用array_merge函数将用户配置的auth覆盖默认值, array_merge后面的数组如果键值和前面的数组一样,将会将前面的数组键值对应的值覆盖*/ if (C("AUTH_CONFIG")) { //可设置配置项 AUTH_CONFIG, 此配置项为数组。 $this->_config = array_merge($this->_config, C("AUTH_CONFIG")); } }
再接着看下面是publick check方法:
首先只我们只看前面一部分,check方法必须传入两个值,后面三个值为可选传入,$name是权限的唯一标识,$uid是用户的id,$type是验证模式,默认为1,也就是时刻认证,$mode是check的模式,$relation是检验$name里面多个权限唯一标识的方式,默认为or,是指多个唯一标识只要满足一个就算成立,如果是and则需要所有的唯一标识全部满足才能成立。
/** * 检查权限 * @param name string|array 需要验证的规则列表,支持逗号分隔的权限规则或索引数组 * @param uid int 认证用户的id * @param type int 是否开启condition验证 * @param string mode 执行check的模式,默认为url,url模式就是index.php?m=mm&c=cc&a=aa这种模式, * 其他模式就是index.php/mm/cc/aa这种模式 * @param relation string 如果为 "or" 表示满足任一条规则即通过验证;如果为 "and"则表示需满足所有规则才能通过验证 * @return boolean 通过验证返回true;失败返回false */ public function check($name, $uid, $type=1, $mode="url", $relation="or") { /*检查auth配置开关是否开启,如果没有开启则return true这样等于是通过验证了*/ if (!$this->_config["AUTH_ON"]) return true; /*通过检验auth认证开启后,使用getAuthList方法获取用户需要验证的规则列表*/ $authList = $this->getAuthList($uid,$type); //获取用户需要验证的所有有效规则列表
/** * 获得权限列表 * @param integer $uid 用户id * @param integer $type 认证方式,默认为1,也就是时刻验证,如果为2则是登录验证 */ /*getAuthList获取用户uid参数以及获取认证方式参数来得到用户的规则列表*/ protected function getAuthList($uid,$type) { /*建立static的$_authList的空数组,用以将来保存权限列表*/ static $_authList = array(); //保存用户验证通过的权限列表 /*如果$type是数组则转换为用,号隔开的字符串以此保证有不合法的类型*/ $t = implode(",",(array)$type); /*检测$_authList静态变量中保存的权限列表有没有键值为$uid.$t的值,如果有的话证明权限存在且通过, return该值通过验证*/ if (isset($_authList[$uid.$t])) { return $_authList[$uid.$t]; } /*检查配置auth_type是否为2登录验证情况系是否存在_AUTH_LIST_.$uid.$t的权限session值, 有的话证明已经验证过权限并存在该权限,返回该session值通过验证*/ if( $this->_config["AUTH_TYPE"]==2 && isset($_SESSION["_AUTH_LIST_".$uid.$t])){ return $_SESSION["_AUTH_LIST_".$uid.$t]; } /*如果既没有存在$_authList对应的值,也不存在session对应的值, 就需要用getGroups来读取用户所属用户组的信息进行查询,该结果为一个数组*/ $groups = $this->getGroups($uid);
下面我们跳到protected getGroups方法里看看:
/** * 根据用户id获取用户组,返回值为数组 * @param uid int 用户id * @return array 用户所属的用户组 array( * array("uid"=>"用户id","group_id"=>"用户组id","title"=>"用户组名称","rules"=>"用户组拥有的规则id,多个,号隔开"), * ...) */ public function getGroups($uid) { /*创建static的$groups空数组来存储用户所属的用户组,用户可以有多个用户组,键值用用户的id表示*/ static $groups = array(); /*检测$groups键值为用户id的值是否存在,如果存在,则返回该用户所拥有的用户组数组*/ if (isset($groups[$uid])) return $groups[$uid]; /*如果$groups键值为用户id的值不存在,新建数据模型 以下实际查询的句子等同: select uid,group_id,title,rules from think_auth_group_access a left join think_auth_group g on a.group_id=g.id where a.uid=1 and g.status=1; */ $user_groups = M() //切换think_auth_group_access表,该表有两个字段,一个用户id,一个对应group_id,该表取了别名a ->table($this->_config["AUTH_GROUP_ACCESS"] . " a") //查询条件为用户id和think_auth_group_access表里的id相等,同时status=1,也就是状态开启 ->where("a.uid="$uid" and g.status="1"") //连接两个表,并设置think_auth_group用户组表别名为g,获取a表group_id=g表的id的行 ->join($this->_config["AUTH_GROUP"]." g on a.group_id=g.id") //查询条件 ->field("uid,group_id,title,rules")->select(); /*如果上面查询结果不为空,那么把该值赋值给$groups的数组,键值为用户id,否则赋空数组*/ $groups[$uid]=$user_groups?:array(); /*将查询到的$groups该用户组的键值对应的权限列表返回*/ return $groups[$uid]; }
这里我们假设得到了$groups[$uid],也就是对应用户id的一个或多个用户组数组,每个用户组数组其中包含用户组id,用户组title,以及用户组rules,下面我们回到getAuthList方法,来接着看下面:
$ids = array();//保存用户所属用户组设置的所有权限规则id /*将查询到的用户组信息进行循环获取每个用户组权限规则id*/ foreach ($groups as $g) { //将规则id字符串去除首尾的,号,然后根据,号分割为数组,并将所有用户组的权限id合并组成$ids数组 $ids = array_merge($ids, explode(",", trim($g["rules"], ","))); } /*移除$ids里重复的权限id*/ $ids = array_unique($ids); /*检测$ids是否为空,如果是则返回权限列表为空数组*/ if (empty($ids)) { $_authList[$uid.$t] = array(); return array(); } /*设置搜索条件$map的数组,$map包含所有该用户拥有的规则id以及满足type还有status=1的条件*/ $map=array( "id"=>array("in",$ids), "type"=>$type, "status"=>1, ); //根据$map查询到所有规则id所对应的权限规则表的condition和name字段 /*等同于:select name,condition from thin_auth_rule where id in ($ids) and type=$type and status=1*/ $rules = M()->table($this->_config["AUTH_RULE"])->where($map)->field("condition,name")->select(); //循环规则,判断结果。 $authList = array(); // foreach ($rules as $rule) { //如果condition字段不为空,根据condition进行验证 if (!empty($rule["condition"])) { $user = $this->getUserInfo($uid);//获取用户信息,一维数组
/** * 获得用户资料,根据自己的情况读取数据库 * @param $uid 用户id * @return 返回用户的一维数组 */ protected function getUserInfo($uid) { /*设置静态变量$userinfo来保存用户数组,先设定空数组*/ static $userinfo=array(); /*检查静态变量$userinfo有没有键值为用户id的对应值,有的话返回该值, 没有进行的话读取数据库查询然后返回该值*/ if(!isset($userinfo[$uid])){ /*这里相当于: select * from think_user where uid=$uid; 因此需要根据自己的情况来更改查询的情况,例如如果condition查询的是积分字段, 假设积分字段为score,用户id字段为id,那么应该更改为: $userinfo[$uid]=M()->field("score")->where(array("id"=>$uid))->table($this->_config["AUTH_USER"])->find(); */ $userinfo[$uid]=M()->where(array("uid"=>$uid))->table($this->_config["AUTH_USER"])->find(); } return $userinfo[$uid]; }
如上面的假设我们得到的$userinfo[$uid]则会是数组,condition字段里面放着该用户的积分字段score的查询结果!
下面再回到getAuthList方法里面,我们接着看:
/*假设条件的格式为{score}>50,这里使用preg_replace替换成$user["score"]>50 并赋值给$command,更具体的可以参考preg_match函数以及正则表达式*/ $command = preg_replace("/{(w*?)}/", "$user["\1"]", $rule["condition"]); //执行$condition的赋值 @(eval("$condition=(" . $command . ");")); /*如果condition变量存在,那么将全部转为小写的$rule["name"]也就是权限的唯一标识赋值给$authList[]数组*/ if ($condition) { $authList[] = strtolower($rule["name"]); } } else { //由于condition字段为空,因此存在权限便将其赋值给$authList[]数组 $authList[] = strtolower($rule["name"]); } } /*将上面循环得到的权限唯一标识数组$authList赋值给$_authList[$uid.$t]以备下次使用*/ $_authList[$uid.$t] = $authList; /*如果配置中的type为2,那么$authList赋值给session*/ if($this->_config["AUTH_TYPE"]==2){ //规则列表结果保存到session里面,以备下次验证使用 $_SESSION["_AUTH_LIST_".$uid.$t]=$authList; } /*返回去掉重复的$authList*/ return array_unique($authList); }
得到返回的权限后,我们重新回到check方法:
/*检查传递进来的$name是否是个字符串*/ if (is_string($name)) { /*转换为小写*/ $name = strtolower($name); /*获取第一个,出现的位置,如果不为false*/ if (strpos($name, ",") !== false) { /*根据,号分解为数组*/ $name = explode(",", $name); } else { /*不存在,号分割的$name则转换为数组*/ $name = array($name); } }这样我们就得到了传递进来的唯一标识数组
/*设置$list为一个空数组,用以保存通过验证的规则名*/ $list = array(); //保存验证通过的规则名 /*检查$mode*/ if ($mode=="url") { /*获取序列化和转变小写再非序列化后的$_REQUEST数组$REQUEST, 该数组里现在有了url里的传值数组*/ $REQUEST = unserialize( strtolower(serialize($_REQUEST)) ); } /*循环$authList的权限规则*/ foreach ( $authList as $auth ) { /*替换掉开始到?的部分*/ $query = preg_replace("/^.+?/U","",$auth); /*如果替换掉的$query不等于$auth,同时$mode是url模式,证明内部带着参数*/ if ($mode=="url" && $query!=$auth ) { /*将$query的参数解析到$param里面去*/ parse_str($query,$param); //解析规则中的param /*比较两者参数的交集*/ $intersect = array_intersect_assoc($REQUEST,$param); /*将以问号后面跟任意个任意字符结尾的替换成空值*/ $auth = preg_replace("/?.*$/U","",$auth); if ( in_array($auth,$name) && $intersect==$param ) { //如果节点相符且url参数满足 $list[] = $auth ; } /*如果check的模式不等于url或者等于url同时$query和$auth相同, 在这个基础上,如果传进来的$name唯一标识符数组里有该层循环的权限规则 把该权限规则存入通过认证权限的$list数组里 */ }else if (in_array($auth , $name)){ $list[] = $auth ; } } /*如果$relation是or参数,认证的权限数组不为空,那么返回true*/ if ($relation == "or" and !empty($list)) { return true; } /*对比$name中有没有$list里没有的,有的话存入$diff变量中*/ $diff = array_diff($name, $list); /*如果$relation是and,他那个是$diff是空值,那么所有的唯一标识都通过了验证, 则返回true*/ if ($relation == "and" and empty($diff)) { return true; } return false; }
以上是整体auth的源码分析!
下面来看一个实例,表的内容就是上面截图的,下面后改动会红字标出
class IndexController extends Controller { public function ceshi(){ $auth=new ThinkAuth(); $a=$auth->check("Index/index,Index/add,Index/delete",1,$type=1, $mode="url", $relation="or"); dump($a); } }
这里三个唯一标识都在用户组1中,而用户id为1的用户组为1,验证关系用的or,就是满足一个唯一标识即可,不考虑条件的情况下这个肯定认证通过,但是type为1,必须对其验证,这个时候如果数据库think_auth_rule的type不为1的话会验证失败,直接返回false,一定要注意,这里的type一定要和数据库里的type一致。
如果把$relation改为and会怎么样呢,验证会通过返回true,因为除开Index/index里有条件设置外,其他都为空,所以存在该权限规则就算通过,而Index/index的积分要求10以上,用户的积分是50,是满足条件的,所以三个权限规则都通过了,返回true。
如果是将Index/index的权限规则改为{score}>60,这个时候就会返回false,虽然其他两个权限规则通过了,但是Index/index规则没有通过,因为积分不足。
可能有些地方讲解的还不尽人意,以后再进行相应的补充,看了源码后觉得auth类在自己用的时候可能需要进行一定的改动方便自己使用会比较好。