• Thinkphp3.2.3中的RBAC权限验证


    最近在用TP的RBAC权限控制,在这里记录学习一下。先来看看相关的概念

    一、相关概念

    访问控制与RBAC模型
    1、访问控制:
            通常的多用户系统都会涉及到访问控制,所谓访问控制,是指通过某种方式允许活限制用户访问能力及范围的一种方法。这主要是由于系统需要对关键资源进行保护,防止由于非法入侵或者误操作对业务系统造成破坏。简而言之,访问控制即哪些用户可以访问哪些资源。
            一般而言,访问控制系统包括三个组成部分:
            主体:发出访问请求的实体,通常指用户或者用户进程。
            客体:被保护的资源,通常是程序,数据,文件或者设备。
            访问策略:主体和客体的映射规则,确定一个主体是否对客体具有访问能力。


    2、RBAC (Role-Base-Access-Controll)
            基于角色的访问控制(RBAC)的概念在七十年代就已经提出,但是直到九十年代由于安全需求的发展才又引起了广泛关注。RBAC 的核心思想是将系统资源的访问权限进行分类或者建立层次关系,抽象为角色的概念,然后根据安全策略将用户和角色关联,从而实现了用户和权限之间的对照。RBAC 通过引入角色并将其作为权限管理的中介,将访问控制系统分为两个部分,即权限与角色的关联和角色与用户的关联,具有灵活易控制的优点。
    来源:http://www.ibm.com/developerworks/cn/java/j-lo-rbacwebsecurity/

    我的理解就是给用户赋予不同的角色,给角色服务不同的权限,权限可以看成要访问资源的集合,角色只是用户和权限的过渡层,可能会问为什要有一个角色过渡层,可以试想一下没有role过渡层,有多个用户有着相同的数个权限,分配管理权限的时候,要重复分配权限,工作量很大,更何况修改权限的时候,更麻烦,可以说不利于代码的维护,所以要有一个role的过渡层。下面来看看TP(thinkphp)中的RBAC的实现及用法。

    二、TP中的RBAC类及使用RBAC类
    1、RBAC权限验证的大致流程:
            ①、验证当前操作是否需要验证
            ②、验证是否登录
            ③、查看当前用户的身份
            ④、获取当前用户的权限列表
            ⑤、进行权限验证

    2、RBAC所依赖的5张数据表

        CREATE TABLE IF NOT EXISTS `think_access` (
          `role_id` smallint(6) unsigned NOT NULL,
          `node_id` smallint(6) unsigned NOT NULL,
          `level` tinyint(1) NOT NULL,
          `module` varchar(50) DEFAULT NULL,
          KEY `groupId` (`role_id`),
          KEY `nodeId` (`node_id`)
        ) ENGINE=MyISAM DEFAULT CHARSET=utf8;
         
        CREATE TABLE IF NOT EXISTS `think_node` (
          `id` smallint(6) unsigned NOT NULL AUTO_INCREMENT,
          `name` varchar(20) NOT NULL,
          `title` varchar(50) DEFAULT NULL,
          `status` tinyint(1) DEFAULT '0',
          `remark` varchar(255) DEFAULT NULL,
          `sort` smallint(6) unsigned DEFAULT NULL,
          `pid` smallint(6) unsigned NOT NULL,
          `level` tinyint(1) unsigned NOT NULL,
          PRIMARY KEY (`id`),
          KEY `level` (`level`),
          KEY `pid` (`pid`),
          KEY `status` (`status`),
          KEY `name` (`name`)
        ) ENGINE=MyISAM  DEFAULT CHARSET=utf8;
         
        CREATE TABLE IF NOT EXISTS `think_role` (
          `id` smallint(6) unsigned NOT NULL AUTO_INCREMENT,
          `name` varchar(20) NOT NULL,
          `pid` smallint(6) DEFAULT NULL,
          `status` tinyint(1) unsigned DEFAULT NULL,
          `remark` varchar(255) DEFAULT NULL,
          PRIMARY KEY (`id`),
          KEY `pid` (`pid`),
          KEY `status` (`status`)
        ) ENGINE=MyISAM  DEFAULT CHARSET=utf8 ;
         
        CREATE TABLE IF NOT EXISTS `think_role_user` (
          `role_id` mediumint(9) unsigned DEFAULT NULL,
          `user_id` char(32) DEFAULT NULL,
          KEY `group_id` (`role_id`),
          KEY `user_id` (`user_id`)
        ) ENGINE=MyISAM DEFAULT CHARSET=utf8;
         
        CREATE TABLE `think_user` (
          `id` int(11) NOT NULL AUTO_INCREMENT,
          `name` varchar(20) DEFAULT '',
          `password` varchar(25) NOT NULL,
          `phone_num` varchar(20) DEFAULT NULL,
          `register_time` int(11) DEFAULT NULL,
          `login_time` int(11) DEFAULT NULL,
          `login_ip` varchar(20) DEFAULT NULL,
          `status` tinyint(1) DEFAULT NULL,
          PRIMARY KEY (`id`)
        ) ENGINE=MyISAM DEFAULT CHARSET=utf8;


    数据表模型如下:

    这五张表的关系如下:
    一个用户对应着多个角色;一个角色可以属于对个用户;是多对多的关系,需要用个中间表即role_user表;
    一个角色可以有多个权限;一个权限可以属于多个用户;是多对多的关系,需要有个中间表即access表;

    那node表示什么?
    node表是记录权限的表,说白了就是记录应用,控制器,及方法的表,即要访问资源的集合;比如要访问的Admin应用下的Index控制器下的index方法;从这里可以明确了TP的RBAC是控制用户对控制器及方法的访问权限进行权限的管理。下面来看个图,加深理解:


    这张图里面的用户,角色,结点,权限及用户与角色中间表一一对应即可(这图是从网上盗的)。其实还可将用户进行抽象,对其进行分组,不同组别里面的角色不同,这可一根据不同的业务进行尝试。可以参考这篇博客
    http://www.cnblogs.com/zwq194/archive/2011/03/07/1974821.html
    知道了RBAC需要的5张数据表,下面来看看RBAC类是如何实现权限验证的。

    3、RBAC类
    RBAC类中三个核心的方法:
        ①getAccessList()获取权限列表

        static public function getAccessList($authId) {
                // Db方式权限数据
                $db     =   Db::getInstance(C('RBAC_DB_DSN'));
                $table = array('role'=>C('RBAC_ROLE_TABLE'),'user'=>C('RBAC_USER_TABLE'),'access'=>C('RBAC_ACCESS_TABLE'),'node'=>C('RBAC_NODE_TABLE'));
                $sql    =   "select node.id,node.name from ".
                            $table['role']." as role,".
                            $table['user']." as user,".
                            $table['access']." as access ,".
                            $table['node']." as node ".
                            "where user.user_id='{$authId}' and user.role_id=role.id and ( access.role_id=role.id  or (access.role_id=role.pid and role.pid!=0 ) ) and role.status=1 and access.node_id=node.id and node.level=1 and node.status=1";
                $apps =   $db->query($sql);
                $access =  array();
                foreach($apps as $key=>$app) {
                    $appId    =    $app['id'];
                    $appName     =     $app['name'];
                    // 读取项目的模块权限
                    $access[strtoupper($appName)]   =  array();
                    $sql    =   "select node.id,node.name from ".
                            $table['role']." as role,".
                            $table['user']." as user,".
                            $table['access']." as access ,".
                            $table['node']." as node ".
                            "where user.user_id='{$authId}' and user.role_id=role.id and ( access.role_id=role.id  or (access.role_id=role.pid and role.pid!=0 ) ) and role.status=1 and access.node_id=node.id and node.level=2 and node.pid={$appId} and node.status=1";
                    $modules =   $db->query($sql);
                    // 判断是否存在公共模块的权限
                    $publicAction  = array();
                    foreach($modules as $key=>$module) {
                        $moduleId     =     $module['id'];
                        $moduleName = $module['name'];
                        if('PUBLIC'== strtoupper($moduleName)) {
                        $sql    =   "select node.id,node.name from ".
                            $table['role']." as role,".
                            $table['user']." as user,".
                            $table['access']." as access ,".
                            $table['node']." as node ".
                            "where user.user_id='{$authId}' and user.role_id=role.id and ( access.role_id=role.id  or (access.role_id=role.pid and role.pid!=0 ) ) and role.status=1 and access.node_id=node.id and node.level=3 and node.pid={$moduleId} and node.status=1";
                            $rs =   $db->query($sql);
                            foreach ($rs as $a){
                                $publicAction[$a['name']]     =     $a['id'];
                            }
                            unset($modules[$key]);
                            break;
                        }
                    }
                    // 依次读取模块的操作权限
                    foreach($modules as $key=>$module) {
                        $moduleId     =     $module['id'];
                        $moduleName = $module['name'];
                        $sql    =   "select node.id,node.name from ".
                            $table['role']." as role,".
                            $table['user']." as user,".
                            $table['access']." as access ,".
                            $table['node']." as node ".
                            "where user.user_id='{$authId}' and user.role_id=role.id and ( access.role_id=role.id  or (access.role_id=role.pid and role.pid!=0 ) ) and role.status=1 and access.node_id=node.id and node.level=3 and node.pid={$moduleId} and node.status=1";
                        $rs =   $db->query($sql);
                        $action = array();
                        foreach ($rs as $a){
                            $action[$a['name']]     =     $a['id'];
                        }
                        // 和公共模块的操作权限合并
                        $action += $publicAction;
                        $access[strtoupper($appName)][strtoupper($moduleName)]   =  array_change_key_case($action,CASE_UPPER);
                    }
                }
                return $access;
            }


    可以看出方法是依次获取项目模块,控制器,动作方法的权限的。

        ②saveAccessList()检测用户的权限列表,并将权限存储到SESSION中

        //用于检测用户权限的方法,并保存到Session中
            static function saveAccessList($authId=null) {
                if(null===$authId)   $authId = $_SESSION[C('USER_AUTH_KEY')];
                // 如果使用普通权限模式,保存当前用户的访问权限列表
                // 对管理员开发所有权限
                if(C('USER_AUTH_TYPE') !=2 && !$_SESSION[C('ADMIN_AUTH_KEY')] )
                    $_SESSION['_ACCESS_LIST']    =    self::getAccessList($authId);
                return ;
            }


        ③AccessDecision()权限决策,就是判断用户拥有哪些权限

        //权限认证的过滤器方法
            static public function AccessDecision($appName=MODULE_NAME) {
                //检查是否需要认证
                if(self::checkAccess()) {
                    //存在认证识别号,则进行进一步的访问决策
                    $accessGuid   =   md5($appName.CONTROLLER_NAME.ACTION_NAME);
                    if(empty($_SESSION[C('ADMIN_AUTH_KEY')])) {
                        if(C('USER_AUTH_TYPE')==2) {
                            //加强验证和即时验证模式 更加安全 后台权限修改可以即时生效
                            //通过数据库进行访问检查
                            $accessList = self::getAccessList($_SESSION[C('USER_AUTH_KEY')]);
                        }else {
                            // 如果是管理员或者当前操作已经认证过,无需再次认证
                            if( $_SESSION[$accessGuid]) {
                                return true;
                            }
                            //登录验证模式,比较登录后保存的权限访问列表
                            $accessList = $_SESSION['_ACCESS_LIST'];
                        }
                        //判断是否为组件化模式,如果是,验证其全模块名
                        if(!isset($accessList[strtoupper($appName)][strtoupper(CONTROLLER_NAME)][strtoupper(ACTION_NAME)])) {
                            $_SESSION[$accessGuid]  =   false;
                            return false;
                        }
                        else {
                            $_SESSION[$accessGuid]    =    true;
                        }
                    }else{
                        //管理员无需认证
                        return true;
                    }
                }
                return true;
            }


    从代码中可以看出,权限判断是有两种方式一种是根据session中的权限列表进行校验,一种是每次都查询数据库进行校验(下面配置参数说明的时候会说明)

        array(3) {
          ["phone"] => string(11) "18792455613"
          ["uid"] => string(2) "13"
          ["_ACCESS_LIST"] => array(1) {
            ["ADMIN"] => array(5) {
              ["ARTICLE"] => array(0) {
              }
              ["CATEGORY"] => array(3) {
                ["CATEGORYLIST"] => string(2) "15"
                ["ADDCATEGORY"] => string(2) "16"
                ["ALTERCATEGORY"] => string(2) "17"
              }
              ["MEDIA"] => array(2) {
                ["IMAGE"] => string(2) "12"
                ["VIDEO"] => string(2) "13"
              }
              ["INDEX"] => array(5) {
                ["USERLIST"] => string(1) "7"
                ["ADDUSER"] => string(1) "8"
                ["ADDROLE"] => string(1) "9"
                ["ADDNODE"] => string(2) "10"
                ["INDEX"] => string(2) "11"
              }
              ["DATA"] => array(0) {
              }
            }
          }
        }


    在AccessDecision()方法中调用checkAccess()方法进行验证模块的过滤即去除不需要验证的模块,控制器和方法。知道了RBAC的实现思路,下面来使用RBAC类进行权限验证。

    4、使用RBAC类进行权限验证

    ①首先处理5张表,就是对5张表的增删该查

    实现逻辑代码如下:建立一个RbacController.class.php的控制器

        /**
             * 角色列表视图
             */
            public function role(){
                $roleList = M("role")->select();
                $this->assign("roleList",$roleList);
                layout("base");
                $this->display();
            }
         
            /**
             * 角色表单处理
             */
            public function roleHandle(){
                $id = I("get.id");
                if(M("role")->where(array("id"=>$id))->delete()){
                    $this->success("删除成功",U("Admin/Index/role"));
                }else{
                    $this->error("删除失败");
                }
            }
         
            /**
             * 结点列表视图
             */
            public function node(){
                $field = array('id','name','title','pid');
                $node = M('node')->field($field)->order('sort')->select();
                $this->node = node_merge($node);
                layout("base");
                $this->display();
            }
         
            /**
             * 权限列表视图
             */
            public function access(){
         
                $rid = I('rid',0,"intval");
                $field = array('id','name','title','pid');
                $node = M('node')->order('sort')->field($field)->select();
                //原有的权限
                $access = M('access')->where(array('role_id'=>$rid))->getField('node_id',true);
         
                $this->node = node_merge($node,$access);
                // dump($this->node);
                $this->rid = $rid;
                layout("base");
                $this->display();
            }
         
            /**
             * 设置权限表单处理
             */
            public function setAccess(){
                $rid = I('rid',0,"intval");
                $db = M('access');
                $db->where(array('role_id'=>$rid))->delete();
                $data = array();
                foreach ($_POST['access'] as $v ) {
                    $tmp = explode('_', $v);
                    $data[] = array(
                        'role_id' => $rid,
                        'node_id' => $tmp[0],
                        'level' => $tmp[1]
                    );
                }
                if ($db->addAll($data)) {
                    $this->success('修改成功',U('Admin/Index/role'));
                }else{
                    $this->error('修改失败');
                }
         
            }
         
            /**
             * 添加角色视图
             */
            public function addRole(){
                layout("base");
                $this->display();
            }
         
            /**
             * 添加角色表单处理
             */
            public function addRoleHandle(){
                $data = I("post.");
                if(M("role")->data($data)->add()){
                    $this->success("添加成功",U("Admin/Index/role"));
                }else{
                    $this->error("添加角色失败,请稍后再试");
                }
            }
         
            /**
             * 添加结点视图
             */
            public function addNode(){
                $this->pid = I("pid",0,"intval");
                $this->level = I("level",1,"intval");
                switch ($this->level){
                    case 1:
                        $this->type = "应用";
                        break;
                    case 2:
                        $this->type = "控制器";
                        break;
                    case 3:
                        $this->type = "方法";
                        break;
                }
                layout("base");
                $this->display();
            }
         
            /**
             * 添加结点表单处理
             */
            public function addNodeHandle(){
                $data = I("post.");
                if(M("node")->data($data)->add()){
                    $this->success("添加成功",U("Admin/Index/node"));
                }else{
                    $this->error("添加失败,请稍后再试");
                }
            }


    其中有一个结点和权限整合的函数node_merge(),实现的思路是递归进行结点添加,将统一控制器中的不同方法整合在一起,代码如下:这段代码放在Common文件夹下的function.php里

        /**
         * 权限结点整合
         * @param  array   $node   结点数组
         * @param  array   $access 权限数组
         * @param  integer $pid    父级ID
         * @return array           组合后的数组
         */
        function node_merge($node,$access=null,$pid=0){
            $arr = array();
            foreach($node as $v){
                if(is_array($access)){
                    $v['access'] = in_array($v['id'],$access) ? 1 : 0;
                }
                if($pid==$v['pid']){
                    $v['child'] = node_merge($node,$access,$v['id']);
                    $arr[] = $v;
                }
            }
            return $arr;
        }


    ②完成了准备工作可以使用RBAC类了,首先先对RABC进行参数配置,下面是配置参数

        配置文件增加设置
        USER_AUTH_ON 是否需要认证
        USER_AUTH_TYPE 认证类型 //1:代表着登录验证2:代表着试试验证
        USER_AUTH_KEY 认证识别号
        REQUIRE_AUTH_MODULE  需要认证模块
        NOT_AUTH_MODULE 无需认证模块
        USER_AUTH_GATEWAY 认证网关 //不是必须的
        RBAC_DB_DSN  数据库连接DSN //不是必须的
        RBAC_ROLE_TABLE 角色表名称
        RBAC_USER_TABLE 用户表名称 //这里是用户和角色的中间表
        RBAC_ACCESS_TABLE 权限表名称
        RBAC_NODE_TABLE 节点表名称


    其中值得注意的是:下面四块

        USER_AUTH_GATEWAY 认证网关 //不是必须的
        RBAC_DB_DSN  数据库连接DSN //不是必须的
        RBAC_USER_TABLE 用户表名称 //这里是用户和角色的中间表
        USER_AUTH_TYPE 认证类型 //1:代表着登录验证2:代表着试试验证


    时时验证是指不将权限信息存入session,而是每访问一个资源,查询数据库进行权限校验;
    登录验证是指将权限信息存入session,而是每访问一个资源,根据session中的信息进行验证;
    可以看出时时验证的安全性更高,但消耗的资源也多。可以看看上面AccessDecision的代码加深理解。

    下面是配置文件的代码:

        <?php
        return array(
            'RBAC_SUPERADMIN'=>'admin',
            'ADMIN_AUTH_KEY' =>'superAdmin', //超级管理员识别
            'USER_AUTH_ON' =>true,  //是否开启权限验证
            'USER_AUTH_TYPE' =>1,   //验证类型(1:登录时验证2:时时验证)
            'USER_AUTH_KEY' =>'uid', //用户验证识别号
            'NOT_AUTH_ACTION' =>'index', // 无需验证的动作方法
            'NOT_AUTH_MODULE' =>'', //无需验证的控制器
            'RBAC_ROLE_TABLE' =>'think_role',//角色表名称
            'RBAC_USER_TABLE' => 'think_role_user',//用户与角色的中间表
            'RBAC_ACCESS_TABLE' =>'think_access',//权限表
            'RBAC_NODE_TABLE' =>'think_node',//节点表
            'URL_HTML_SUFFIX' =>'',
            // 'SHOW_PAGE_TRACE' => true,
        );


    ③开始“真正”使用RABC类进行权限验证
        阐述下我用到的两个类:
        LoginController.class.php处理用户登录请求;
        CommonController.class.php添加_initialize()函数,让其他控制器继承此类;这里值得说明的是_initialize()函数,他会在调用其他方法之前执行,这也就是为什么要在这个函数中进行权限验证;

      One: 使用登录验证方式:首先在登录的时候向session中写入权限LoginController.class.php

        <?php
        /**
         * Created by PhpStorm.
         * User: zhangpeng
         * Date: 2016/1/29
         * Time: 12:37
         */
         
        namespace AdminController;
         
        use OrgUtilRbac;
        use ThinkController;
         
        class LoginController extends Controller
        {
            /**
             * 登录首页视图
             */
            public function index()
            {
                $this->display();
            }
         
            /**
             * 登录表单处理
             */
            public function loginHandle()
            {
                if (IS_POST) {
                    $userModel = M('user');
                    $where = array(
                        'phone_num' => I('post.phone_num'),
                        'password' => md5(I('post.password'))
                    );
                    $result = $userModel->where($where)->find();
                    if (!$result) {
                        $this->error("登陆失败");
                    }else{
                        session("phone",I('post.phone_num'));
                        session("uid",$result['id']);
         
                        if($result['name']==C('RBAC_SUPERADMIN')){
                            session(C('ADMIN_AUTH_KEY'),true);
                        }
         
                        //将权限写入session
                        Rbac::saveAccessList();
                        $this->success('欢迎登陆',U('Admin/Index/index'));
                    }
                }
            }
         
            /**
             * 登出操作
             */
            public function logout(){
                if(session("phone")){
                    session("uid",null);
                    session("phone",null);
                    session(null);
                }
                $this->redirect("Login/index");
            }
        }




    其次,在_initialize()函数中进行权限验证:CommonController.class.php

        <?php
        /**
         * Created by PhpStorm.
         * User: zhangpeng
         * Date: 2016/2/1
         * Time: 23:28
         */
         
        namespace AdminController;
         
         
        use OrgUtilRbac;
        use ThinkController;
         
        /**
         * Class CommonController
         * @package AdminController
         * 用户验证用户的状态,实现用户页面保留的功能
         */
        class CommonController extends Controller
        {
            /**
             * 自动执行函数,tp中自带
             */
            public function _initialize(){
                if(!session("uid")){
                    $this->redirect('Login/index');
                }
         
                if(C('USER_AUTH_ON')){
                    Rbac::AccessDecision()||$this->error("你没有权限");
                }
            }
        }


    Two:时时验证,只需要在_initialize()函数中添加验证即可,代码不在赘述。

    其中值得注意的是,我用的是TP3.2.3,此时TP支持命名空间和类自动加载机制,所以不像老版本需要用import引入RBAC类才能使用。

    三、总结
    RBAC类短短的200多行代码实现了权限控制的需求,感觉十分厉害。后续把我的demo代码上传,与大家一起学习。
    ---------------------
    作者:benettzhang
    来源:CSDN
    原文:https://blog.csdn.net/zp_00000/article/details/51236719
    版权声明:本文为博主原创文章,转载请附上博文链接!

  • 相关阅读:
    Python 类和对象
    Python zxing 库解析(条形码二维码识别)
    MFC&Halcon之实时视频监控
    MFC&Halcon之图片显示
    Halcon11与VS2010联合开发
    堆排序程序中的小于等于号问题
    cenos7 u disk install
    UML类图关系表示
    socket http1
    mfc http
  • 原文地址:https://www.cnblogs.com/gyrgyr/p/10873575.html
Copyright © 2020-2023  润新知