thinkphp和onethink之权限管理

onethink权限管理主要分为两个方面一种菜单节点检测,另一种是动态检测(未实现)。
第一次进入系统后,在Admin/Controller/AdminController.class.php中权限验证的代码为:

          define('IS_ROOT',   is_administrator());
                if(!IS_ROOT && C('ADMIN_ALLOW_IP')){
                    // 检查IP地址访问
                    if(!in_array(get_client_ip(),explode(',',C('ADMIN_ALLOW_IP')))){
                        $this->error('403:禁止访问');
                    }
                }
                 $access =   $this->accessControl();
                if ( $access === false ) {
                    $this->error('403:禁止访问');
                }elseif( $access === null ){
                    $dynamic        =   $this->checkDynamic();//动态检测的代码,返回null
                    if( $dynamic === null ){
                        //检测非动态权限
                        $rule  = strtolower(MODULE_NAME.'/'.CONTROLLER_NAME.'/'.ACTION_NAME);
                        if(!IS_ROOT) {
                            if (!$this->checkRule($rule, array('in', '1,2'))) {

                                $this->error('未授权访问!');
                                exit;
                            }
                        }
                    }elseif( $dynamic === false ){
                        $this->error('未授权访问!');
                    }
                }

在onethink的数据库中有四张表是和权限管理有关联的,这里写图片描述
其中rule表对应的是此系统中所有的url生成的规则表,group表对应的是某个分组所拥有的权限,也就是某个分组可以访问的url集合。group_access代表的某个用户属于某个组,extend表主要用来实现动态检测。

/Admin/Controller/AdminController.class.php中进行的第一次权限检测,

     /**
     * action访问控制,在 **登陆成功** 后执行的第一项权限检测任务
     *
     * @return boolean|null  返回值必须使用 `===` 进行判断
     *
     *   返回 **false**, 不允许任何人访问(超管除外)
     *   返回 **true**, 允许任何管理员访问,无需执行节点权限检测
     *   返回 **null**, 需要继续执行节点权限检测决定是否允许访问
     * 
     */
  final protected function accessControl(){
        $allow = C('ALLOW_VISIT');
        $deny  = C('DENY_VISIT');#这两项配置存储在config表中
        $check = strtolower(CONTROLLER_NAME.'/'.ACTION_NAME);
        if ( !empty($deny)  && in_array_case($check,$deny) ) {
            return false;//非超管禁止访问deny中的方法
        }
        if ( !empty($allow) && in_array_case($check,$allow) ) {
            return true;
        }
        return null;//需要检测节点权限
    }

权限认证的配置在/ThinkPHP/Library/Think/Auth.class.php中如图:
thinkphp和onethink之权限管理_第1张图片
规则验证中最重要的函数为check()函数:

    public function check($name, $uid, $type=1, $mode='url', $relation='or') {

        if (!$this->_config['AUTH_ON'])#如果没有开启验证,返回true
            return true;
        $authList = $this->getAuthList($uid,$type); //获取用户拥有的权限列表

        if (is_string($name)) {
            $name = strtolower($name);
            if (strpos($name, ',') !== false) { #如果是多个,将其拆分成数组
                $name = explode(',', $name);
            } else {
                $name = array($name);
            }
        }

        $list = array(); //保存验证通过的规则名
        if ($mode=='url') {

            $REQUEST = unserialize( strtolower(serialize($_REQUEST)) );

        }
        foreach ( $authList as $auth ) {

            $query = preg_replace('/^.+\?/U','',$auth);#获得参数字符串
            if ($mode=='url' && $query!=$auth ) {

                parse_str($query,$param); //解析规则中的param 生成一个数组,键值对对应url中的键值对

                $intersect = array_intersect_assoc($REQUEST,$param);#输出$REQUEST 和$param的交集
                $auth = preg_replace('/\?.*$/U','',$auth);#此时的$auth为url路径
                if ( in_array($auth,$name) && $intersect==$param ) {  //如果节点相符且url参数满足

                    $list[] = $auth ;
                }
                }else if (in_array($auth , $name)){#遍历用户拥有的权限数组,如果某个权限存在于$name数组中,则将其放入$list数组,假设用户拥有权限为1,2,3,4,5,
                                                    #需要验证的权限为2,6.那么会将2放入$list数组,
                $list[] = $auth ;
            }
        }
        exit;
        if ($relation == 'or' and !empty($list)) {#如上个例子中,当为或时,只要$list数组不为空,既只要满足一个权限就可以
            return true;
        }
        $diff = array_diff($name, $list);
        if ($relation == 'and' and empty($diff)) {#如上例中,当为与时,需要满足$List数组和$name数组完全相同才可以,既$name中的权限全部存在于$auth中
            return true;
        }
        return false;
    }

因为后台的控制器都继承了AdminController控制器,所以每打开一个url,都会首先检测改用户是否具有权限。
进入后台后,进入到用户的权限管理页面,如默认用户组,执行的方法为:

    public function access(){
        $this->updateRules();//首先执行此方法,此方法根据menu表中的数据更新rule表中的数据,具体见下方代码

        $auth_group = M('AuthGroup')->where( array('status'=>array('egt','0'),'module'=>'admin','type'=>AuthGroupModel::TYPE_ADMIN) )
                                    ->getfield('id,id,title,rules');

        $node_list   = $this->returnNodes();//查询menu表,获得主菜单数组以及子菜单数组

        $map         = array('module'=>'admin','type'=>AuthRuleModel::RULE_MAIN,'status'=>1);
        $main_rules  = M('AuthRule')->where($map)->getField('name,id');//查询rule表获得主菜单的url和id值

        $map         = array('module'=>'admin','type'=>AuthRuleModel::RULE_URL,'status'=>1);
        $child_rules = M('AuthRule')->where($map)->getField('name,id');//查询rule表获得子菜单的url和id值

        $this->assign('main_rules', $main_rules);
        $this->assign('auth_rules', $child_rules);
        $this->assign('node_list',  $node_list);

        $this->assign('auth_group', $auth_group);
        $this->assign('this_group', $auth_group[(int)$_GET['group_id']]);//当前用户组
        $this->meta_title = '访问授权';
        $this->display('managergroup');
    }
  public function updateRules(){
        //需要新增的节点必然位于$nodes
        $nodes    = $this->returnNodes(false); #returnNodes查询出表menu中的所有菜单项,生成一个二维数组,其中的一个值如下:
    /*    0 => array:4 [▼
     *    "title" => "文档列表"
     *  "url" => "Admin/article/index"
     *"tip" => ""
     *"pid" => "2"
     *    ]
    */
        $AuthRule = M('AuthRule');
        $map      = array('module'=>'admin','type'=>array('in','1,2'));
        //需要更新和删除的节点必然位于$rules
        $rules    = $AuthRule->where($map)->order('name')->select();//查询出属于admin模块的所有规则,其中type=1代表url,type=2代表主菜单

        //构建insert数据
        $data     = array();//保存需要插入和更新的新节点
        foreach ($nodes as $value){
            $temp['name']   = $value['url'];
            $temp['title']  = $value['title'];
            $temp['module'] = 'admin';
            if($value['pid'] >0){
                $temp['type'] = AuthRuleModel::RULE_URL;//RULE_URL为1代表url
            }else{
                $temp['type'] = AuthRuleModel::RULE_MAIN;//RULE_MAIN为2代表主菜单
            }
            $temp['status']   = 1;
            $data[strtolower($temp['name'].$temp['module'].$temp['type'])] = $temp;//去除重复项
        }
        /*$data的一个子数组如下:此时$data存储的为menu表中的数据
         *   "admin/article/indexadmin1" => array:5 [▼
         *   "name" => "Admin/article/index"
         *   "title" => "文档列表"
         *   "module" => "admin"
         *   "type" => 1
         *   "status" => 1
          ]
         */
        $update = array();//保存需要更新的节点
        $ids    = array();//保存需要删除的节点的id

        foreach ($rules as $index=>$rule){//$data是菜单生成的数组,此循环的作用是根据菜单数组,来进行规则表的增删改操作,如果规则数组中的某个键和菜单数组的键相同则将菜单数组
                                        //中的该值放入$updata表,将规则数组的值放入$diff表,如果规则数组中某个值不存在与菜单数组中,说明规则数组中的该值需要删除
            $key = strtolower($rule['name'].$rule['module'].$rule['type']);
            if ( isset($data[$key]) ) {//如果数据库中的规则与配置的节点匹配,说明是需要更新的节点
                $data[$key]['id'] = $rule['id'];//为需要更新的节点补充id值

                $update[] = $data[$key];
                unset($data[$key]);
                unset($rules[$index]);
                unset($rule['condition']);
                $diff[$rule['id']]=$rule;
            }elseif($rule['status']==1){
                $ids[] = $rule['id'];
            }
        }

        if ( count($update) ) { //$update是菜单表生成的,$diff是规则表生成的
            foreach ($update as $k=>$row){
                if ( $row!=$diff[$row['id']] ) {//判断菜单数组的数据是否有更新,如果有更新,规则表也进行更新

                    $AuthRule->where(array('id'=>$row['id']))->save($row);
                }
            }
        }
        if ( count($ids) ) { //
            $AuthRule->where( array( 'id'=>array('IN',implode(',',$ids)) ) )->save(array('status'=>-1));
            //删除规则是否需要从每个用户组的访问授权表中移除该规则?
        }
        //需要更新的$data已经unset掉,剩余的数据为为新增数据,执行add操作
        if( count($data) ){
            $AuthRule->addAll(array_values($data));//array_values函数将关联数组变为索引数组,只作用的一维
        }
        if ( $AuthRule->getDbError() ) {
            trace('['.__METHOD__.']:'.$AuthRule->getDbError());
            return false;
        }else{
            return true;
        }
    }


生成菜单数据后,view层使用三层循环将数据输出,循环的数据如内容
thinkphp和onethink之权限管理_第2张图片

                <volist name="node_list" id="node" >//第一次循环主菜单
                                        <dl class="checkmod">
                                            <dt class="hd">
                                                <label class="checkbox"><input class="auth_rules rules_all" type="checkbox" name="rules[]" value="">{$node.title}管理label>
                                            dt>
                                            <dd class="bd">
                                                <present name="node['child']">
                                                <volist name="node['child']" id="child" > //第二次循环子菜单
                                                    <div class="rule_check">
                                                        <div>
                                                            <label class="checkbox" <notempty name="child['tip']">title='{$child.tip}'notempty>>
                                                           <input class="auth_rules rules_row" type="checkbox" name="rules[]" value=""/>{$child.title}
                                                            label>
                                                        div>
                                                            <notempty name="child['operator']">
                                                               <span class="child_row">
                                                                   <volist name="child['operator']" id="op"> //第三次循环操作
                                                                       <label class="checkbox" <notempty name="op['tip']">title='{$op.tip}'notempty>>
                                                                           <input class="auth_rules" type="checkbox" name="rules[]"
                                                                           value=""/>{$op.title}
                                                                       label>
                                                                   volist>
                                                               span>
                                                            notempty>
                                                        div>
                                                volist>
                                                present>
                                            dd>
                                        dl>
                                    volist>

对于如何生成菜单数据主要调用了两个函数为:returnNodes()和函数list_to_tree(),
returnNodes()函数的代码为:

 final protected function returnNodes($tree = true){
        static $tree_nodes = array();
        if ( $tree && !empty($tree_nodes[(int)$tree]) ) {
            return $tree_nodes[$tree];
        }

        if((int)$tree){
            $list = M('Menu')->field('id,pid,title,url,tip,hide')->order('sort asc')->select();
            foreach ($list as $key => $value) {  //给$list数组的url字段加上模块名
                if( stripos($value['url'],MODULE_NAME)!==0 ){
                    $list[$key]['url'] = MODULE_NAME.'/'.$value['url'];
                }
            }
            $nodes = list_to_tree($list,$pk='id',$pid='pid',$child='operator',$root=0);//将菜单生成树形结构

            foreach ($nodes as $key => $value) {
                if(!empty($value['operator'])){
                    $nodes[$key]['child'] = $value['operator'];//将键名由operator更改为child
                    unset($nodes[$key]['operator']);
                }
            }

        }else{//返回一维数组
            $nodes = M('Menu')->field('title,url,tip,pid')->order('sort asc')->select();
            foreach ($nodes as $key => $value) {
                if( stripos($value['url'],MODULE_NAME)!==0 ){
                    $nodes[$key]['url'] = MODULE_NAME.'/'.$value['url'];
                }
            }
        }
        $tree_nodes[(int)$tree]   = $nodes;
        return $nodes;
    }

list_to_tree()函数的代码为:

function list_to_tree($list, $pk='id', $pid = 'pid', $child = '_child', $root = 0) {
    // 创建Tree

    $tree = array();
    if(is_array($list)) {
        // 创建基于主键的数组引用
        $refer = array();

        foreach ($list as $key => $data) {
            $refer[$data[$pk]] = & $list[$key];//将$list数组以引用的方式转换成$refer数组,键为子数组的id值
        }

        foreach ($list as $key => $data) {
            // 判断是否存在parent
            $parentId =  $data[$pid];
            if ($root == $parentId) {//此时pid = 0为主菜单,直接放入$tree数组
                $tree[] =& $list[$key];
            }else{
                if (isset($refer[$parentId])) {//此时当前url的父菜单在$refer中
                    $parent =& $refer[$parentId];
                    $parent[$child][] =& $list[$key];
//                    dump($parent);
                }
            }
        }
    }

    return $tree;
}

thinkphp和onethink之权限管理_第3张图片
函数list_to_tree()仅使用的几行代码就生成了一个树,现分析如下 :
$parent =& $refer[$parentId]是以引用的方式赋值,所以改变$parent的值,就相当于改变$refer的值,又因为 $refer[$data[$pk]] = $list[$key], 所以改变$refer的值就相当于改变$list的值,又因为$tree[] =& $list[$key]所以改变$list的值就相当于改变$tree的值,总结为:改变了$parent的值就相当于改变了$tree的值,以上图为例,它是生成的树形结构中的用户分类,当遍历到用户信息时,在$refer中含有用户这个数组,所以会在用户这个数组中添加一个子元素,键为operator,值为用户信息这个数组,当遍历到新增用户时,同样查找$refer,在$refer这个数组中含有用户信息这个数组,所以给用户信息这个数组添加一个子元素,键为operator,值为新增用户这个数组,因为使用引用的关系,所以$tree数组的每一个元素都是到此函数执行到最后一步才确定的,比如当用户信息添加了子元素新增用户时,用户这个数组也会跟着进行变动。

你可能感兴趣的:(onethink)