RBAC基于角色的权限访问控制

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

RBAC是Role-Based Access Control的首字母,译成中文即基于角色的权限访问控制,说白了也就是用户经过角色与权限进行关联[其架构灵感来源于操做系统的GBAC(GROUP-Based Access Control)的权限管理控制]。简单的来讲,一个用户能够拥有若干角色,每个角色拥有若干权限。这样,就构形成“用户-角色-权限”的受权模型。在这种模型中,用户与角色之间,角色与权限之间,通常者是多对多的关系。其对应关系以下:
image
在许多的实际应用中,系统不仅是须要用户完成简单的注册,还须要对不一样级别的用户对不一样资源的访问具备不一样的操做权限。且在企业开发中,权限管理系统也成了重复开发效率最高的一个模块之一。而在多套系统中,对应的权限管理只能知足自身系统的管理须要,不管是在数据库设计、权限访问和权限管理机制方式上均可能不一样,这种不致性也就存在以下的憋端: php

  • 维护多套系统,重复造轮子,时间没用在刀刃上
  • 用户管理、组织机制等数据重复维护,数据的完整性、一致性很可贵到保障
  • 权限系统设计不一样,概念理解不一样,及相应技术差别,系统之间集成存在问题,单点登陆难度大,也复杂的企业系统带来困难

RBAC是基于不断实践以后,提出的一个比较成熟的访问控制方案。实践代表,采用基于RBAC模型的权限管理系统具备如下优点:html

  • 因为角色、权限之间的变化比角色、用户关系之间的变化相对要慢得多,减少了受权管理的复杂性,下降管理开销;
  • 并且可以灵活地支持应用系统的安全策略,并对应用系统的变化有很大的伸缩性;
  • 在操做上,权限分配直观、容易理解,便于使用;分级权限适合分层的用户级形式;
  • 重用性强。

ThinkPHP中RBAC实现体系

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

  • 安全拦截器
  • 认证管理器
  • 决策访问管理器
  • 运行身份管理器

安全拦截器

安全拦截器就比如一道道门,在系统的安全防御系统中可能存在不少不一样的安全控制环节,一旦某个环节你未经过安全体系认证,那么安全拦截器就会实施拦截。算法

认证管理器

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

访问决策管理

虽然经过了认证管理器的身份验证,可是并不表明你能够在系统里面肆意妄为,由于你还须要经过访问决策管理这道门。访问决策管理器对用户进行受权,经过考虑你的身份认证信息和与受保护资源关联的安全属性决定是是否能够进入系统的某个模块,和进行某项操做。例如,安全规则规定只有主管才容许访问某个模块,而你并无被授予主管权限,那么安全拦截器会拦截你的访问操做。
决策访问管理器不能单独运行,必须首先依赖认证管理器进行身份确认,所以,在加载访问决策过滤器的时候已经包含了认证管理器和决策访问管理器。
为了知足应用的不一样须要,ThinkPHP 在进行访问决策管理的时候采用两种模式:登陆模式和即时模式。登陆模式,系统在用户登陆的时候读取改用户所具有的受权信息到 Session,下次再也不从新获取受权信息。也就是说即便管理员对该用户进行了权限修改,用户也必须在下次登陆后才能生效。即时模式就是为了解决上面的问题,在每次访问系统的模块或者操做时候,进行即便验证该用户是否具备该模块和操做的受权,从更高程度上保障了系统的安全。thinkphp

运行身份管理器

运行身份管理器的用处在大多数应用系统中是有限的,例如某个操做和模块须要多个身份的安全需求,运行身份管理器能够用另外一个身份替换你目前的身份,从而容许你访问应用系统内部更深处的受保护对象。这一层安全体系目前的 RBAC 中还没有实现。数据库

ThinkPHP中RBAC认证流程

对应上面的安全体系,ThinkPHP 的 RBAC 认证的过程大体以下:数组

  1. 判断当前模块的当前操做是否须要认证
  2. 若是须要认证而且还没有登陆,跳到认证网关,若是已经登陆 执行5
  3. 经过委托认证进行用户身份认证
  4. 获取用户的决策访问列表
  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用户表,这个根据业务本身定义session

字段名 字段类型 做用
id INT 用户ID(惟一识别号)
username VARCHAR(16) 用户名
password VARCHAR(32) 密码
email 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





附件列表