ThinkPHP中RBAC实现体系
安全拦截器
认证管理器
访问决策管理
运行身份管理器
ThinkPHP中RBAC认证流程
权限管理的具体实现过程
RBAC相关的数据库介绍
ThinkPHP的RBAC处理类
实际使用
登录校验
自动校验权限状态
参考
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语句如下(复制后请替换表前缀):
// 配置文件增加设置 // ADMIN_AUTH_KEY 管理员的认证键名 // USER_AUTH_ON 是否需要认证 // USER_AUTH_TYPE 认证类型 1 登录认证 2 实时认证 见AccessDecision函数。 // 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 节点表名称 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;
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) | 备注信息 |
role角色表
字段名 | 字段类型 | 作用 |
---|---|---|
id | INT | 角色ID |
name | VARCHAR(20) | 角色名称 |
pid | SMALLINT(6) | 父角色对应ID |
status | TINYINT(1) | 启用状态(同上) |
remark | VARCHAR(255) | 备注信息 |
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:表示方法 |
role_user用户角色关系表
字段名 | 字段类型 | 作用 |
---|---|---|
user_id | INT | 用户ID |
role_id | SMALLINT(6) | 角色ID |
access权限表
字段名 | 字段类型 | 作用 |
---|---|---|
role_id | SMALLINT(6) | 角色ID |
node_id | SMALLINT(6) | 节点ID |
level | TINYINT(1) | 冗余节点表界别? |
module | VARCHAR(50) | 模块说明? |
ThinkPHP的RBAC处理类
1 class Rbac { 2 // 认证方法,$map参数根据用户表自定义,只要能校验用户名密码就行。 3 static public function authenticate($map,$model='') { 4 if(empty($model)) $model = C('USER_AUTH_MODEL'); 5 //使用给定的Map进行认证 6 return M($model)->where($map)->find(); 7 } 8 9 //用于检测用户权限的方法,并保存到Session中 10 static function saveAccessList($authId=null) { 11 if(null===$authId) $authId = $_SESSION[C('USER_AUTH_KEY')]; 12 // 如果使用普通权限模式,保存当前用户的访问权限列表 13 // 对管理员开发所有权限 14 if(C('USER_AUTH_TYPE') !=2 && !$_SESSION[C('ADMIN_AUTH_KEY')] ) 15 $_SESSION['_ACCESS_LIST'] = self::getAccessList($authId); 16 return ; 17 } 18 19 // 取得模块的所属记录访问权限列表 返回有权限的记录ID数组 20 static function getRecordAccessList($authId=null,$module='') { 21 if(null===$authId) $authId = $_SESSION[C('USER_AUTH_KEY')]; 22 if(empty($module)) $module = CONTROLLER_NAME; 23 //获取权限访问列表 24 $accessList = self::getModuleAccessList($authId,$module); 25 return $accessList; 26 } 27 28 //检查当前操作是否需要认证 29 static function checkAccess() { 30 //如果项目要求认证,并且当前模块需要认证,则进行权限认证 31 if( C('USER_AUTH_ON') ){ 32 $_module = array(); 33 $_action = array(); 34 if("" != C('REQUIRE_AUTH_MODULE')) { 35 //需要认证的模块 36 $_module['yes'] = explode(',',strtoupper(C('REQUIRE_AUTH_MODULE'))); 37 }else { 38 //无需认证的模块 39 $_module['no'] = explode(',',strtoupper(C('NOT_AUTH_MODULE'))); 40 } 41 //检查当前模块是否需要认证 42 if((!empty($_module['no']) && !in_array(strtoupper(CONTROLLER_NAME),$_module['no'])) || (!empty($_module['yes']) && in_array(strtoupper(CONTROLLER_NAME),$_module['yes']))) { 43 if("" != C('REQUIRE_AUTH_ACTION')) { 44 //需要认证的操作 45 $_action['yes'] = explode(',',strtoupper(C('REQUIRE_AUTH_ACTION'))); 46 }else { 47 //无需认证的操作 48 $_action['no'] = explode(',',strtoupper(C('NOT_AUTH_ACTION'))); 49 } 50 //检查当前操作是否需要认证 51 if((!empty($_action['no']) && !in_array(strtoupper(ACTION_NAME),$_action['no'])) || (!empty($_action['yes']) && in_array(strtoupper(ACTION_NAME),$_action['yes']))) { 52 return true; 53 }else { 54 return false; 55 } 56 }else { 57 return false; 58 } 59 } 60 return false; 61 } 62 63 // 登录检查 64 static public function checkLogin() { 65 //检查当前操作是否需要认证 66 if(self::checkAccess()) { 67 //检查认证识别号 68 if(!$_SESSION[C('USER_AUTH_KEY')]) { 69 if(C('GUEST_AUTH_ON')) { 70 // 开启游客授权访问 71 if(!isset($_SESSION['_ACCESS_LIST'])) 72 // 保存游客权限 73 self::saveAccessList(C('GUEST_AUTH_ID')); 74 }else{ 75 // 禁止游客访问跳转到认证网关 76 redirect(PHP_FILE.C('USER_AUTH_GATEWAY')); 77 } 78 } 79 } 80 return true; 81 } 82 83 //权限认证的过滤器方法 84 static public function AccessDecision($appName=MODULE_NAME) { 85 //检查是否需要认证 86 if(self::checkAccess()) { 87 //存在认证识别号,则进行进一步的访问决策 88 $accessGuid = md5($appName.CONTROLLER_NAME.ACTION_NAME); 89 if(empty($_SESSION[C('ADMIN_AUTH_KEY')])) { 90 if(C('USER_AUTH_TYPE')==2) { 91 //加强验证和即时验证模式 更加安全 后台权限修改可以即时生效 92 //通过数据库进行访问检查 93 $accessList = self::getAccessList($_SESSION[C('USER_AUTH_KEY')]); 94 }else { 95 // 如果是管理员或者当前操作已经认证过,无需再次认证 96 if( $_SESSION[$accessGuid]) { 97 return true; 98 } 99 //登录验证模式,比较登录后保存的权限访问列表 100 $accessList = $_SESSION['_ACCESS_LIST']; 101 } 102 //判断是否为组件化模式,如果是,验证其全模块名 103 if(!isset($accessList[strtoupper($appName)][strtoupper(CONTROLLER_NAME)][strtoupper(ACTION_NAME)])) { 104 $_SESSION[$accessGuid] = false; 105 return false; 106 } 107 else { 108 $_SESSION[$accessGuid] = true; 109 } 110 }else{ 111 //管理员无需认证 112 return true; 113 } 114 } 115 return true; 116 } 117 118 /** 119 +---------------------------------------------------------- 120 * 取得当前认证号的所有权限列表,看起来有点晕,仔细阅读。 121 +---------------------------------------------------------- 122 * @param integer $authId 用户ID 123 +---------------------------------------------------------- 124 * @access public 125 +---------------------------------------------------------- 126 */ 127 static public function getAccessList($authId) { 128 // Db方式权限数据 129 $db = Db::getInstance(C('RBAC_DB_DSN')); 130 $table = array('role'=>C('RBAC_ROLE_TABLE'),'user'=>C('RBAC_USER_TABLE'),'access'=>C('RBAC_ACCESS_TABLE'),'node'=>C('RBAC_NODE_TABLE')); 131 $sql = "select node.id,node.name from ". 132 $table['role']." as role,". 133 $table['user']." as user,". 134 $table['access']." as access ,". 135 $table['node']." as node ". 136 "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"; 137 $apps = $db->query($sql); 138 $access = array(); 139 foreach($apps as $key=>$app) { 140 $appId = $app['id']; 141 $appName = $app['name']; 142 // 读取项目的模块权限 143 $access[strtoupper($appName)] = array(); 144 $sql = "select node.id,node.name from ". 145 $table['role']." as role,". 146 $table['user']." as user,". 147 $table['access']." as access ,". 148 $table['node']." as node ". 149 "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"; 150 $modules = $db->query($sql); 151 // 判断是否存在公共模块的权限 152 $publicAction = array(); 153 foreach($modules as $key=>$module) { 154 $moduleId = $module['id']; 155 $moduleName = $module['name']; 156 if('PUBLIC'== strtoupper($moduleName)) { 157 $sql = "select node.id,node.name from ". 158 $table['role']." as role,". 159 $table['user']." as user,". 160 $table['access']." as access ,". 161 $table['node']." as node ". 162 "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"; 163 $rs = $db->query($sql); 164 foreach ($rs as $a){ 165 $publicAction[$a['name']] = $a['id']; 166 } 167 unset($modules[$key]); 168 break; 169 } 170 } 171 // 依次读取模块的操作权限 172 foreach($modules as $key=>$module) { 173 $moduleId = $module['id']; 174 $moduleName = $module['name']; 175 $sql = "select node.id,node.name from ". 176 $table['role']." as role,". 177 $table['user']." as user,". 178 $table['access']." as access ,". 179 $table['node']." as node ". 180 "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"; 181 $rs = $db->query($sql); 182 $action = array(); 183 foreach ($rs as $a){ 184 $action[$a['name']] = $a['id']; 185 } 186 // 和公共模块的操作权限合并 187 $action += $publicAction; 188 $access[strtoupper($appName)][strtoupper($moduleName)] = array_change_key_case($action,CASE_UPPER); 189 } 190 } 191 return $access; 192 } 193 194 // 读取模块所属的记录访问权限 195 static public function getModuleAccessList($authId,$module) { 196 // Db方式 197 $db = Db::getInstance(C('RBAC_DB_DSN')); 198 $table = array('role'=>C('RBAC_ROLE_TABLE'),'user'=>C('RBAC_USER_TABLE'),'access'=>C('RBAC_ACCESS_TABLE')); 199 $sql = "select access.node_id from ". 200 $table['role']." as role,". 201 $table['user']." as user,". 202 $table['access']." as access ". 203 "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.module='{$module}' and access.status=1"; 204 $rs = $db->query($sql); 205 $access = array(); 206 foreach ($rs as $node){ 207 $access[] = $node['node_id']; 208 } 209 return $access; 210 } 211 }
实际使用
登录校验
// 用户登录页面,如果已经登陆过,直接跳转主页 public function login() { //RBAC类里面也包含了checkLogin函数,用哪个随意。 if(!isset($_SESSION[C('USER_AUTH_KEY')])) { $this->display(); }else{ $this->redirect('Index/index'); } } // 用户登出,清空相关session参数 public function logout() { if(isset($_SESSION[C('USER_AUTH_KEY')])) { unset($_SESSION[C('USER_AUTH_KEY')]); unset($_SESSION); session_destroy(); $this->success('登出成功!',__URL__.'/login/'); }else { $this->error('已经登出!'); } } // 检查用户是否登录 protected function checkUser() { //RBAC类里面也包含了checkLogin函数,用哪个随意。 if(!isset($_SESSION[C('USER_AUTH_KEY')])) { $this->error('没有登录','Public/login'); } } // 登录检测 public function checkLogin() { if(empty($_POST['account'])) { $this->error('帐号错误!'); }elseif (empty($_POST['password'])){ $this->error('密码必须!'); }elseif (empty($_POST['verify'])){ $this->error('验证码必须!'); } //生成认证条件 $map = array(); // 首先使用账号密码校验是否正确,这里的account等参数和数据库用户表设计相关。 $map['account'] = $_POST['account']; $map["status"] = array('gt',0); //先检测验证码 if(session('verify') != md5($_POST['verify'])) { $this->error('验证码错误!'); } //开始使用RBAC校验。 import ( '@.ORG.Util.RBAC' ); //这里会使用配置文件USER_AUTH_MODEL设置的model来校验,去用户表查看用户密码是否正确。 $authInfo = RBAC::authenticate($map); //使用用户名、密码和状态的方式进行认证 if(false === $authInfo) { $this->error('帐号不存在或已禁用!'); }else { //这里的密码比对根据自己的密码加盐算法进行修改,默认是用MD5 if($authInfo['password'] != md5($_POST['password'])) { $this->error('密码错误!'); } //校验没问题,设置好session会话参数,类似authInfo的参数和用户表的设计有关。 $_SESSION[C('USER_AUTH_KEY')] = $authInfo['id']; $_SESSION['email'] = $authInfo['email']; $_SESSION['loginUserName'] = $authInfo['nickname']; $_SESSION['lastLoginTime'] = $authInfo['last_login_time']; $_SESSION['login_count'] = $authInfo['login_count']; //不一定是admin就是管理员,这个也看系统设计,见配置文件ADMIN_AUTH_KEY if($authInfo['account']=='admin') { $_SESSION['administrator'] = true; } //保存这一次的登录信息 $User = M('User'); $ip = get_client_ip(); $time = time(); $data = array(); $data['id'] = $authInfo['id']; $data['last_login_time'] = $time; $data['login_count'] = array('exp','login_count+1'); $data['last_login_ip'] = $ip; $User->save($data); // 用于检测用户权限的方法,并保存到Session中 RBAC::saveAccessList(); $this->success('登录成功!',__APP__.'/Index/index'); } }
自动校验权限状态
在Controller或者Action中初始化函数校验是否登录,然后继承即可
1 function _initialize() { 2 import('@.ORG.Util.Cookie'); 3 // 用户权限检查,只检查需要校验的模块 4 if (C('USER_AUTH_ON') && !in_array(MODULE_NAME, explode(',', C('NOT_AUTH_MODULE')))) { 5 import('@.ORG.Util.RBAC'); 6 //判断是否有权限,见源码。 7 if (!RBAC::AccessDecision()) { 8 //检查认证识别号 9 if (!$_SESSION [C('USER_AUTH_KEY')]) { 10 //跳转到认证网关 11 redirect(PHP_FILE . C('USER_AUTH_GATEWAY')); 12 } 13 // 没有权限 抛出错误 14 if (C('RBAC_ERROR_PAGE')) { 15 // 定义权限错误页面 16 redirect(C('RBAC_ERROR_PAGE')); 17 } else { 18 //开启游客验证,跳转登录界面 19 if (C('GUEST_AUTH_ON')) { 20 $this->assign('jumpUrl', PHP_FILE . C('USER_AUTH_GATEWAY')); 21 } 22 // 提示错误信息 23 $this->error(L('_VALID_ACCESS_')); 24 } 25 } 26 } 27 }
总的来说,读完源码,再实际使用一遍,绝对搞定~
thinkphp demo下载地址
链接:http://pan.baidu.com/s/1ge5pkll 密码:qds8
参考
http://www.lyblog.net/detail/552.html
http://www.thinkphp.cn/extend/235.html