RBAC是什么,能解决什么难题?
RBAC 是Role-Based Access Control的首字母,译成中文即基于角色的权限访问控制,说白了也就是用户通过角色与权限进行关联[其架构灵感来源于操作系统的 GBAC(GROUP-Based Access Control)的权限管理控制]。简单的来说,一个用户可以拥有若干角色,每一个角色拥有若干权限。这样,就构造成“用户-角色-权限”的授权模型。在 这种模型中,用户与角色之间,角色与权限之间,一般者是多对多的关系。其对应关系如下:
在 许多的实际应用中,系统不只是需要用户完成简单的注册,还需要对不同级别的用户对不同资源的访问具有不同的操作权限。且在企业开发中,权限管理系统也成了 重复开发效率最高的一个模块之一。而在多套系统中,对应的权限管理只能满足自身系统的管理需要,无论是在数据库设计、权限访问和权限管理机制方式上都可能 不同,这种不致性也就存在如下的憋端:
维护多套系统,重复造轮子,时间没用在刀刃上
用户管理、组织机制等数据重复维护,数据的完整性、一致性很难得到保障
权限系统设计不同,概念理解不同,及相应技术差异,系统之间集成存在问题,单点登录难度大,也复杂的企业系统带来困难
RBAC 是基于不断实践之后,提出的一个比较成熟的访问控制方案。实践表明,采用基于RBAC模型的权限管理系统具有以下优势:由于角色、权限之间的变化比角色、 用户关系之间的变化相对要慢得多,减小了授权管理的复杂性,降低管理开销;而且能够灵活地支持应用系统的安全策略,并对应用系统的变化有很大的伸缩性;在 操作上,权限分配直观、容易理解,便于使用;分级权限适合分层的用户级形式;重用性强。
ThinkPHP中RBAC实现体系
ThinkPHP中RBAC基于Java的Spring的Acegi安全系统作为参考原型,并做了相应的简化处理,以适应当前的ThinkPHP结构,提供一个多层、可定制的安全体系来为应用开发提供安全控制。安全体系中主要有以下几部分:
安全拦截器
认证管理器
决策访问管理器
运行身份管理器
安全拦截器
安全拦截器就好比一道道门,在系统的安全防护系统中可能存在很多不同的安全控制环节,一旦某个环节你未通过安全体系认证,那么安全拦截器就会实施拦截。
认证管理器
防护体系的第一道门就是认证管理器,认证管理器负责决定你是谁,一般它通过验证你的主体(通常是一个用户名)和你的凭证(通常是一个密码),或者更多的资料来做到。更简单的说,认证管理器验证你的身份是否在安全防护体系授权范围之内。
访问决策管理
虽
然通过了认证管理器的身份验证,但是并不代表你可以在系统里面肆意妄为,因为你还需要通过访问决策管理这道门。访问决策管理器对用户进行授权,通过考虑你
的身份认证信息和与受保护资源关联的安全属性决定是是否可以进入系统的某个模块,和进行某项操作。例如,安全规则规定只有主管才允许访问某个模块,而你并
没有被授予主管权限,那么安全拦截器会拦截你的访问操作。
决策访问管理器不能单独运行,必须首先依赖认证管理器进行身份确认,因此,在加载访问决策过滤器的时候已经包含了认证管理器和决策访问管理器。
为
了满足应用的不同需要,ThinkPHP
在进行访问决策管理的时候采用两种模式:登录模式和即时模式。登录模式,系统在用户登录的时候读取改用户所具备的授权信息到
Session,下次不再重新获取授权信息。也就是说即使管理员对该用户进行了权限修改,用户也必须在下次登录后才能生效。即时模式就是为了解决上面的问
题,在每次访问系统的模块或者操作时候,进行即使验证该用户是否具有该模块和操作的授权,从更高程度上保障了系统的安全。
运行身份管理器
运行身份管理器的用处在大多数应用系统中是有限的,例如某个操作和模块需要多个身份的安全需求,运行身份管理器可以用另一个身份替换你目前的身份,从而允许你访问应用系统内部更深处的受保护对象。这一层安全体系目前的 RBAC 中尚未实现。
ThinkPHP中RBAC认证流程
对应上面的安全体系,ThinkPHP 的 RBAC 认证的过程大致如下:
判断当前模块的当前操作是否需要认证
如果需要认证并且尚未登录,跳到认证网关,如果已经登录 执行5
通过委托认证进行用户身份认证
获取用户的决策访问列表
判断当前用户是否具有访问权限
权限管理的具体实现过程
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相关的数据库表及字段作一下介绍:
表名 | 字段名 | 字段类型 | 作用 |
---|---|---|---|
ly_user | id | INT | 用户ID(唯一识别号) |
username | VARCHAR(16) | 用户名 | |
password | VARCHAR(32) | 密码 | |
VARCHAR(100) | 用户邮箱 | ||
create_time | TIMESTAMP | 创建时间(时间戳) | |
logintime | TIMESTAMP | 最近一次登录时间(时间戳) | |
loginip | VARCHAR(15) | 最近登录的IP地址 | |
status | TINYINT(1) | 启用状态:0:表示禁用;1:表示启用 | |
remark | VARCHAR(255) | 备注信息 | |
ly_role | id | INT | 角色ID |
name | VARCHAR(20) | 角色名称 | |
pid | SMALLINT(6) | 父角色对应ID | |
status | TINYINT(1) | 启用状态(同上) | |
remark | VARCHAR(255) | 备注信息 | |
ly_node | id | SMALLINT(6) | 节点ID |
name | VARCHAR(20) | 节点名称(英文名,对应应用控制器、应用、方法名) | |
title | VARCHAR(50) | 节点中文名(方便看懂) | |
status | TINYINT(1) | 启用状态(同上) | |
remark | VARCHAR(255) | 备注信息 | |
sort | SMALLINT(6) | 排序值(默认值为50) | |
pid | SMALLINT(6) | 父节点ID(如:方法pid对应相应的控制器) | |
level | TINYINT(1) | 节点类型:1:表示应用(模块);2:表示控制器;3:表示方法 | |
ly_role_user | user_id | INT | 用户ID |
role_id | SMALLINT(6) | 角色ID | |
ly_access | role_id | SMALLINT(6) | 角色ID |
node_id | SMALLINT(6) | 节点ID | |
level | |||
module |
以下是数据库表各字段的关联关系:
实现RBAC管理的前导性工作
基于ThinkPHP实现RBAC的权限管理系统中,首先要做一些前导性的工作(系统数据库设计TP已经为我们完成了),主要分以下几个方面:
用户(增、删、改、查)
角色(增、删、改、查)
节点(增、删、改、查)
配置权限(更新权限)
具体实现的代码如下(相关解释均在注释之中):
复制
<?php/** * Created by PhpStorm. * User: LiuYang * Date: 14-9-6 * Time: 下午9:54 * Description: 基于ThinkPHP实现的权限管理系统 */ 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' => '权限更新失败' )); } }}
ThinkPHP中RBAC类的详解
在ThinkPHP处理权限管理中,真正的难点并不是在RBAC类的使用上,上面相关的处理(权限配置,节点管理等);所以当完成以上工作,其实RBAC系统已经完成了90%。下面先从ThinkPHP中RBAC的配置说起(详细请参看对应的注释内容):
复制
<?php/** * Created by PhpStorm. * User: LiuYang * Date: 14-9-29 * Time: 下午9:36 * Description: ThinkPHP中RBAC处理类的配置文件 */ 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', //节点表名称(必配));
注意:
以上有的配置项并非必须的,但标有“必配”,则必须配置
无需认证的权限和方法与需要认证的模块和动作可以根据需要选择性配置,还有部分权限相关配置并未列表出
RBAC处理类提供静态的方法
ThinkPHP的RBAC处理类提供给我们很多相关的静态方法如下:
方法名 | 接收参数说明 | 返回值 | 说明 |
---|---|---|---|
RBAC::authenticate($map,$model=''); |
| array |
返回值是在用户表中,以$map为条件 |
0RBAC::saveAccessList($authId=null); |
| 返回一个空值 | 如果验证方式为登录验证,则将权限写入session中,否则不作任何处理 |
RBAC::getRecordAccessList($authId=null,$module=''); |
| Array | 返回一个包含权限的ID的数组 |
RBAC::checkAccess() | 无 | 返回true或false | 检查当前操作是否需要认证(根据配置中需要认证和不需要评论的模块或方法得出) |
RBAC::checkLogin() | 无 | true | 如果当前操作需要认证且用户没有登录,继续检测是否开启游客授权。如果开启游客授权,则写入游客权限;否则跳到登录页 |
RBAC::AccessDecision($appName=APP_NAME) |
|
| AccessDecision($appName=APP_NAME)方法,检测当前项目模块操作,是否存在 于$_SESSION['_ACCESS_LIST']数组中$_SESSION['_ACCESS_LIST']['当前操作']['当前模块'][' 当前操作']是否存在。如果存在表示有权限,返回true;否则返回flase。 |
RBAC::getAccessList($authId) |
| Array | 通过数据库查询取得当前认证号的所有权限列表 |
RBAC::getModuleAccessList($authId,$module) |
| Array | 返回指定用户可访问的节点权限数组 |
注意:在使用RBAC::AccessDecision()
方法时,如果你开启了项目分组,则必须传入当前分组,代码如下:
复制
//权限验证if(C('USER_AUTH_ON') && !$notAuth) { import('ORG.Util.RBAC'); //使用了项目分组,则必须引入GROUP_NAME RBAC::AccessDecision(GROUP_NAME) || $this->error("你没有对应的权限");}
RBAC处理类的实际应用
在完成用户登录,角色创建,节点增删改查的工作后,就只剩下了RBAC如何在对应程序代码中应用了。挻简单的,只用在原来的代码其他上改动几个地方即可。
用户登录时,写入用户权限
用户操作时,进行权限验证
下面是用户登录时的实现代码:
复制
>?php/** * Created by PhpStorm. * User: LiuYang * Date: 14-8-24 * Time: 下午5:23 * Description: 用户登户及退出控制器 */ 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/** * Created by PhpStorm. * User: LiuYang * Date: 14-9-3 * Time: 下午8:40 * Description: 操作权限验证 */ 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()
方法。
本文以上实例代码及相关源码下载:
人无完人,肯定有不足之处,望拍砖指点(本文完)。
原创文章,转载请注明出处:小天地,大世界[http://www.lyblog.net]
文章地址:http://www.lyblog.net/detail/552.html