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类在自己用的时候可能需要进行一定的改动方便自己使用会比较好。
