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
附件列表
