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, <span style="white-space:pre;"> </span>// 认证方式,1为实时认证;2为登录认证。
- 'AUTH_GROUP' => 'auth_group', // 用户组数据表名
- 'AUTH_GROUP_ACCESS' => 'auth_group_access', <span style="white-space:pre;"> </span>// 用户-用户组关系表
- '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); //获取用户需要验证的所有有效规则列表
下面我们跳跃到auth类里的getAuthList()方法看一下:
- /**
- * 获得权限列表
- * @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);//获取用户信息,一维数组
下面要根据condition来验证条件,例如如果用用户的积分来验证,所以这里通过获取一个数组:
- /**
- * 获得用户资料,根据自己的情况读取数据库
- * @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 \Think\Auth();
- $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类在自己用的时候可能需要进行一定的改动方便自己使用会比较好。