zoukankan      html  css  js  c++  java
  • 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
    版权声明:本文为博主原创文章,转载请附上博文链接!

  • 相关阅读:
    VS 2008潜在强大的功能:提取EXE文件中的ICO等资源
    园友们注意:淘宝网上QQ会员 4钻 3元 等都为骗子行为
    Comet Async Process Request Handler
    WCF(Sender) to MSMQ to WCF(Receiver)
    ASP.NET Web Form GridView DetailsView Query Edit
    WCF NetTcp AsyncQueue Service
    Xml CDATA 序列化
    Sync Invoke Remoting Async Invoke
    .Net 4.0 Remoting ConcurrentQueue
    Socket Async Receive Data to LinkedList Buffer (telnet proxy server)
  • 原文地址:https://www.cnblogs.com/gyrgyr/p/10873575.html
Copyright © 2011-2022 走看看