14、ThinkPHP中关于RBAC使用详解

阅读() @2018-07-15 14:13:38

一、RBAC是什么,能解决什么难题?

RBAC是Role-Based Access Control的首字母,译成中文即基于角色的权限访问控制,说白了也就是用户通过角色与权限进行关联[其架构灵感来源于操作系统的GBAC(GROUP-Based Access Control)的权限管理控制]。简单的来说,一个用户可以拥有若干角色,每一个角色拥有若干权限。这样,就构造成“用户-角色-权限”的授权模型。在这种模型中,用户与角色之间,角色与权限之间,一般者是多对多的关系。其对应关系如下:

RBAC是什么

在许多的实际应用中,系统不只是需要用户完成简单的注册,还需要对不同级别的用户对不同资源的访问具有不同的操作权限。且在企业开发中,权限管理系统也成了重复开发效率最高的一个模块之一。而在多套系统中,对应的权限管理只能满足自身系统的管理需要,无论是在数据库设计、权限访问和权限管理机制方式上都可能不同,这种不致性也就存在如下的憋端:

1、维护多套系统,重复造轮子,时间没用在刀刃上;

2、用户管理、组织机制等数据重复维护,数据的完整性、一致性很难得到保障;

3、权限系统设计不同,概念理解不同,及相应技术差异,系统之间集成存在问题,单点登录难度大,也复杂的企业系统带来困难。

RBAC是基于不断实践之后,提出的一个比较成熟的访问控制方案。实践表明,采用基于RBAC模型的权限管理系统具有以下优势:由于角色、权限之间的变化比角色、用户关系之间的变化相对要慢得多,减小了授权管理的复杂性,降低管理开销;而且能够灵活地支持应用系统的安全策略,并对应用系统的变化有很大的伸缩性;在操作上,权限分配直观、容易理解,便于使用;分级权限适合分层的用户级形式;重用性强。

二、ThinkPHP中RBAC实现体系

ThinkPHP中RBAC基于Java的Spring的Acegi安全系统作为参考原型,并做了相应的简化处理,以适应当前的ThinkPHP结构,提供一个多层、可定制的安全体系来为应用开发提供安全控制。安全体系中主要有以下几部分:

1、安全拦截器:

安全拦截器就好比一道道门,在系统的安全防护系统中可能存在很多不同的安全控制环节,一旦某个环节你未通过安全体系认证,那么安全拦截器就会实施拦截。

2、认证管理器:

防护体系的第一道门就是认证管理器,认证管理器负责决定你是谁,一般它通过验证你的主体(通常是一个用户名)和你的凭证(通常是一个密码),或者更多的资料来做到。更简单的说,认证管理器验证你的身份是否在安全防护体系授权范围之内。

3、决策访问管理器:

虽然通过了认证管理器的身份验证,但是并不代表你可以在系统里面肆意妄为,因为你还需要通过访问决策管理这道门。访问决策管理器对用户进行授权,通过考虑你的身份认证信息和与受保护资源关联的安全属性决定是是否可以进入系统的某个模块,和进行某项操作。例如,安全规则规定只有主管才允许访问某个模块,而你并没有被授予主管权限,那么安全拦截器会拦截你的访问操作。 

决策访问管理器不能单独运行,必须首先依赖认证管理器进行身份确认,因此,在加载访问决策过滤器的时候已经包含了认证管理器和决策访问管理器。 

为了满足应用的不同需要,ThinkPHP 在进行访问决策管理的时候采用两种模式:登录模式和即时模式。登录模式,系统在用户登录的时候读取改用户所具备的授权信息到 Session,下次不再重新获取授权信息。也就是说即使管理员对该用户进行了权限修改,用户也必须在下次登录后才能生效。即时模式就是为了解决上面的问题,在每次访问系统的模块或者操作时候,进行即使验证该用户是否具有该模块和操作的授权,从更高程度上保障了系统的安全。

4、运行身份管理器:

运行身份管理器的用处在大多数应用系统中是有限的,例如某个操作和模块需要多个身份的安全需求,运行身份管理器可以用另一个身份替换你目前的身份,从而允许你访问应用系统内部更深处的受保护对象。这一层安全体系目前的 RBAC 中尚未实现。

三、ThinkPHP中RBAC认证流程

对应上面的安全体系,ThinkPHP 的 RBAC 认证的过程大致如下:

1、判断当前模块的当前操作是否需要认证;

2、如果需要认证并且尚未登录,跳到认证网关,如果已经登录 执行5;

3、通过委托认证进行用户身份认证;

4、获取用户的决策访问列表;

5、判断当前用户是否具有访问权限。

四‘、权限管理的具体实现过程

1、RBAC相关的数据库介绍

在ThinkPHP完整包,包含了RBAC处理类RBAC.class.php文件,位于Extend/Library/ORG/Util。打开该文件,其中就包含了使用RBAC必备的4张表,SQL语句如下(复制后请替换表前缀):
CREATE TABLE IF NOT EXISTS `ly_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 `ly_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 `ly_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 `ly_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;

下面对RBAC相关的数据库表及字段作一下介绍:

RBAC相关数据库表字段详解

以下是数据库表各字段的关联关系:

RBAC相关数据库表字段之间的联系关系

2、实现RBAC管理的前导性工作:

基于ThinkPHP实现RBAC的权限管理系统中,首先要做一些前导性的工作(系统数据库设计TP已经为我们完成了),主要分以下几个方面:

用户(增、删、改、查)、角色(增、删、改、查)、节点(增、删、改、查)、配置权限(更新权限)。

具体实现的代码如下(相关解释均在注释之中):

<?php
 
class RbacAction extends CommonAction {
 
    //初始化操作
    function _initialize(){
        if(!IS_AJAX) $this->error('你访问的页面不存在,请稍后再试');
    }
 
 
    //用户列表
    public function index(){
        $db = M('user');
 
        //当前页码
        $pageNum = I('post.pageNum',1,'int');
        //每页显示条数
        $numPerPage = I('post.numPerPage',C("numPerPage"),'int');
        //总页码数
        $totalCount = $db->count();
 
        $this->totalCount = $totalCount;
        $this->numPerPage = $numPerPage;
        $this->items = D('UserRelation')->relation(true)->page($pageNum, $numPerPage)->select();
        $this->display();
    }
 
    //添加编辑用户弹层表单
    public function addUser(){
        //如果设置了uid,则为编辑用户,否则为增加用户
        $this->role = M('role')->where('status = 1')->field('id,name')->select();
 
        if(isset($_GET['uid'])) {
            $this->userinfo = M('user')->where( "id = $_GET[uid]" )->find();
        }
        $this->display();
    }
 
    //添加及编辑用户表单处理
    public function addUserHandler(){
 
        $db = M('user');
        if($_POST['id']) {
            //如果存在ID,即表示更新
            $data = array(
                'id' => I('post.id','','int'),
                'username' => I('username', '', 'string'),
                'status' => I('status','', 'int'),
                'remark' => I('remark'),
                'logintime' => time(),
                'loginip' => get_client_ip()
            );
 
            if($_POST['password']) $data['password'] = I('password','', 'md5');
            if($db->save($data)) {
                $roleuser = M('role_user');
 
                $roleuser->where("id = $data[id]")->delete();
 
                $roleuser->add(array(
                    'role_id' => I('role','','intval'),
                    'user_id' => $data[id]
                ));
 
                $this->ajaxReturn(array(
                    'statusCode' => 200,
                    'message' => '更新成功'
                ));
            } else {
                $this->ajaxReturn(array(
                    'statusCode' => 300,
                    'message' => '操作失败'
                ));
            }
 
            return ;
        }
 
        //添加表单处理
        $data = array(
            'username' => I('username', '', 'string'),
            'password' => I('password', '', 'md5'),
            'status' => I('status','', 'int'),
            'remark' => I('remark'),
            'logintime' => time(),
            'loginip' => get_client_ip()
        );
        if($uid = M('user')->add($data)) {
            $roleuser = M('role_user');
 
            $roleuser->where("id = $uid")->delete();
 
            $roleuser->add(array(
                'role_id' => I('role','','intval'),
                'user_id' => $uid
            ));
 
            $this->ajaxReturn(array(
                'statusCode' => 200,
                'message' => '操作成功',
                'navTabId' => '',
                'rel' => '',
                'callbackType' => '',
                'forwardUrl' => '',
                'confirmMsg' => ''
            ));
        } else {
            $this->ajaxReturn(array(
                'statusCode' => 300,
                'message' => '操作失败'
            ));
        }
    }
 
    //启用或清除用户
    public function resume(){
        $id = I('get.id','0','int');
        $db = M('user');
        $status = $db->where("id = $id")->getField('status');
        $status = ($status == 1)? 0 : 1 ;
        if($db->where("id = $id")->setField('status', $status)){
            $this->ajaxReturn(array(
                'statusCode' => 1,
                'message' => '操作成功',
                'navTabId' =>$_GET['navTabId']
            ));
        } else {
            $this->ajaxReturn(array(
                'statusCode' => 0,
                'message' => '操作失败'
            ));
        }
    }
 
    //删除用户
    public function deleteUserHandler(){
        $id = I('get.uid',0,'int');
        if( M('user')->delete($id) ) {
            $this->ajaxReturn(array(
                'statusCode' => 1,
                'message' => '删除成功',
                'navTabId' => $_GET['navTabId']
            ));
        } else {
            $this->ajaxReturn(array(
                'statusCode' => 0,
                'message' => '删除成功',
                'navTabId' => $_GET['navTabId']
            ));
        }
    }
 
    //节点列表
    public function node(){
        $node = M('node')->where(array('status'=>1))->order('sort')->select();
        $this->node = node_merge($node);
        $this->display();
    }
 
    //添加及编辑节点弹层表单
    public function addNode(){
        //添加表单默认情况
        $this->info = array(
            'level' => I('get.level',1,'int'),
            'pid' => I('get.pid',0,'int'),
            'status' => 1,
            'sort' => 50
        );
        switch ($this->info['level']){
            case 1: {
                $this->label = "应用";
                break;
            }
            case 2: {
                $this->label = "控制器";
                break;
            }
            case 3: {
                $this->label = "方法";
                break;
            }
        }
        if($_GET['id']) {
            //编辑模式
            $this->info = M('node')->where(array('id'=>$_GET['id']))->find();
        }
        $this->display();
    }
 
    //添加及编辑节点表单处理
    public function addNodeHandler(){
        $id = $_POST['id'];
        $db = M('node');
        if($id) {
            //更新
            if($db->save($_POST)) {
                $this->ajaxReturn(array(
                    'statusCode' => 200,
                    'message' => '添加成功',
                    'navTabId' => $_GET['navTabId']
                ));
            } else {
                $this->ajaxReturn(array(
                    'statusCode' => 300,
                    'message' => '更新失败',
                    'navTabId' => $_GET['navTabId']
                ));
            }
        }else {
            //保存
            if($db->add($_POST)) {
                $this->ajaxReturn(array(
                    'statusCode' => 200,
                    'message' => '添加成功',
                    'navTabId' => $_GET['navTabId']
                ));
            } else {
                $this->ajaxReturn(array(
                    'statusCode' => 300,
                    'message' => '添加失败',
                    'navTabId' => $_GET['navTabId']
                ));
            }
        }
    }
 
    //删除节点
    public function deleteNodeHandler(){
        $id = I('get.id','0','int');
        $db = M('node');
        $data = $db->where(array('pid'=>$id))->field('id')->find();
        if($data) {
            $this->ajaxReturn(array(
                'statusCode' => 0,
                'message' => '你请求删除的节点存在子节点,不可直接删除'
            ));
        } else {
            if($db->delete($id)) {
                $this->ajaxReturn(array(
                    'statusCode'=> 1,
                    'message' => '删除成功'
                ));
            } else {
                $this->ajaxReturn(array(
                    'statusCode' => 0,
                    'message' => '删除失败'
                ));
            }
        }
        //if($data['level'] === 3)
    }
 
    //角色管理
    public function role(){
        $this->role = M('role')->select();
        $this->display();
    }
 
    //添加及编辑角色
    public function addRole(){
        if($_GET['rid']) {
            $id = I('get.rid',0,'int');
            $this->role = M('role')->find($id);
        }
        $this->display();
    }
 
    //添加角色表单处理
    public function addRoleHandler(){
        $db = M('role');
        if($_POST['id']) {
            //更新
            if($db->save($_POST)) {
                $this->ajaxReturn(array(
                    'statusCode'=> 200,
                    'message' => "角色信息更新成功"
                ));
            } else {
                $this->ajaxReturn(array(
                    'statusCode' => "300",
                    'message' => '角色信息更新失败'
                ));
            }
        } else {
            //添加表单处理
            if($db ->add($_POST)){
                $this->ajaxReturn(array(
                    'statusCode'=> 200,
                    'message' => "角色添加成功"
                ));
            }else {
                $this->ajaxReturn(array(
                    'statusCode' => 300,
                    'message' => '角色添加失败'
                ));
            }
        }
    }
 
    //删除角色
    public function deleteRole(){
 
    }
 
    //快束启用或楚用用户
    public function resumeRole(){
        $id = I('get.rid',0, 'int');
        $db = M('role');
        $status = $db->where("id = $id")->getField('status');
        $status = ($status == 1)? 0 : 1 ;
        if($db->where("id = $id")->setField('status', $status)){
            $this->ajaxReturn(array(
                'statusCode' => 1,
                'message' => '操作成功',
                'navTabId' =>$_GET['navTabId']
            ));
        } else {
            $this->ajaxReturn(array(
                'statusCode' => 0,
                'message' => '操作失败'
            ));
        }
    }
 
 
    //给用户添加节点权限
    public function access(){
        $rid = I('rid',0 ,'intval');
        $node = M('node')->where(array('status'=>1))->field(array('id','title','pid','name','level'))->order('sort')->select();
 
        //获取原有权限
        $access = M('access')->where("role_id = $rid")->getField('node_id',true);
        $this->node = node_merge($node,$access);
        $this->assign('rid',$rid)->display();
    }
 
    //添加节点权限表单处理
    public function accessHandler(){
        $rid = I('rid', '', 'intval');
        $db = M('access');
        //清空原有权限
        $db->where("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->ajaxReturn(array(
                'statusCode'=> 200,
                'message' => '权限更新成功'
            ));
        } else {
            $this->ajaxReturn(array(
                'statusCode' => 300,
                'message' => '权限更新失败'
            ));
        }
 
    }
}

3、ThinkPHP中RBAC类的详解:

在ThinkPHP处理权限管理中,真正的难点并不是在RBAC类的使用上,上面相关的处理(权限配置,节点管理等);所以当完成以上工作,其实RBAC系统已经完成了90%。下面先从ThinkPHP中RBAC的配置说起(详细请参看对应的注释内容):

<?php 
return array(
 
    "USER_AUTH_ON" => true,                    //是否开启权限验证(必配)
    "USER_AUTH_TYPE" => 1,                     //验证方式(1、登录验证;2、实时验证)
 
    "USER_AUTH_KEY" => 'uid',                  //用户认证识别号(必配)
    "ADMIN_AUTH_KEY" => 'superadmin',          //超级管理员识别号(必配)
    "USER_AUTH_MODEL" => 'user',               //验证用户表模型 ly_user
    'USER_AUTH_GATEWAY'  =>  '/Public/login',  //用户认证失败,跳转URL
 
    'AUTH_PWD_ENCODER'=>'md5',                 //默认密码加密方式
 
    "RBAC_SUPERADMIN" => 'admin',              //超级管理员名称
 
 
    "NOT_AUTH_MODULE" => 'Index,Public',       //无需认证的控制器
    "NOT_AUTH_ACTION" => 'index',              //无需认证的方法
 
    'REQUIRE_AUTH_MODULE' =>  '',              //默认需要认证的模块
    'REQUIRE_AUTH_ACTION' =>  '',              //默认需要认证的动作
 
    'GUEST_AUTH_ON'   =>  false,               //是否开启游客授权访问
    'GUEST_AUTH_ID'   =>  0,                   //游客标记
 
    "RBAC_ROLE_TABLE" => 'ly_role',            //角色表名称(必配)
    "RBAC_USER_TABLE" => 'ly_role_user',       //用户角色中间表名称(必配)
    "RBAC_ACCESS_TABLE" => 'ly_access',        //权限表名称(必配)
    "RBAC_NODE_TABLE" => 'ly_node',            //节点表名称(必配)
);

相关推荐:《ThinkPHP中config配置文件详解》。

注意:以上有的配置项并非必须的,但标有“必配”,则必须配置。无需认证的权限和方法与需要认证的模块和动作可以根据需要选择性配置,还有部分权限相关配置并未列表出。

4、RAC处理类提供静态B的方法:

RBAC处理类提供静态的方法:

RBAC处理类提供的静态方法

注意:在使用RBAC::AccessDecision()方法时,如果你开启了项目分组,则必须传入当前分组,代码如下:

//权限验证
if(C('USER_AUTH_ON') && !$notAuth) {
    import('ORG.Util.RBAC');
    //使用了项目分组,则必须引入GROUP_NAME
    RBAC::AccessDecision(GROUP_NAME) || $this->error("你没有对应的权限");
}

5、RBAC处理类的实际应用:

在完成用户登录,角色创建,节点增删改查的工作后,就只剩下了RBAC如何在对应程序代码中应用了。挻简单的,只用在原来的代码其他上改动几个地方即可。

(1)用户登录时,写入用户权限;

(2)用户操作时,进行权限验证。

下面是用户登录时的实现代码:

<?php
 
class LoginAction extends Action {
 
    //用户登录视图
    public function index(){
       //...
    }
 
    //用户登录处理表单
    public function loginHandle(){
        if(!IS_POST) halt('页面不存在,请稍后再试');
        if(session('verify') != I('param.verify','','md5')) {
            $this->error('验证码错误', U('Admin/Login/index'));
        }
 
        $user = I('username','','string');
        $passwd = I('password','','md5');
 
        $db = M('user');
        $userinfo = $db->where("username = '$user' AND password = '$passwd'")->find();
 
        if(!$userinfo) $this->error('用户名或密码错误', U('Admin/Login/index'));
 
        if(!$userinfo['status']) $this->error('该用户被锁定,暂时不可登录', U('Admin/Login/index'));
 
        //更新登录信息
        $db->save(array("id"=> $userinfo["id"], "logintime"=> time(), "loginip" => get_client_ip()));
 
        //写入session值
        session(C("USER_AUTH_KEY"), $userinfo["id"]);
        session("username", $userinfo["username"]);
        session("logintime", $userinfo["logintime"]);
        session("loginip",$user["loginip"]);
 
        //如果为超级管理员,则无需验证
        if($userinfo['username'] == C('RBAC_SUPERADMIN')) {
            session(C('ADMIN_AUTH_KEY'), true);
        }
 
        //载入RBAC类
        import('ORG.Util.RBAC');
        //读取用户权限
        RBAC::saveAccessList();
 
        $this->success('登录成功', U('Admin/Index/index'));
    }
 
    //登出登录
    public function logOut(){
       //...
    }
 
    //验证码
    public function verify(){
        //...
    }
}

接着在控制器目录创建一个CommonAction.class.php文件,然后改写所有要权限验证的类,让其继承自CommonAction。代码如下:

<?php
 
class CommonAction extends Action {
    function _initialize(){
        if(!isset($_SESSION[C('USER_AUTH_KEY')])) {
            $this->redirect('Admin/Login/index');
        }
 
        $notAuth = in_array(MODULE_NAME, explode(',', C('NOT_AUTH_MODULE'))) || in_array(ACTION_NAME, C('NOT_AUTH_ACTION'));
 
        //权限验证
        if(C('USER_AUTH_ON') && !$notAuth) {
            import('ORG.Util.RBAC');
            //使用了项目分组,则必须引入GROUP_NAME
            RBAC::AccessDecision(GROUP_NAME) || $this->error("你没有对应的权限");
        }
    }
}

在ThinkPHP中提供了一个_initialize()方法,是在类初始化就会执行的,也就是只要后面控制器继承自CommonAction类,就会在作对应操作时,执行_initialize()方法。

(完)!

微信二维码
锐壳主机