Thinkphp中的RBAC許可權驗證
一、相關概念
訪問控制與RBAC模型
1、訪問控制:
通常的多使用者系統都會涉及到訪問控制,所謂訪問控制,是指通過某種方式允許活限制使用者訪問能力及範圍的一種方法。這主要是由於系統需要對關鍵資源進行保護,防止由於非法入侵或者誤操作對業務系統造成破壞。簡而言之,訪問控制即哪些使用者可以訪問哪些資源。
一般而言,訪問控制系統包括三個組成部分:
主體:發出訪問請求的實體,通常指使用者或者使用者程序。
客體:被保護的資源,通常是程式,資料,檔案或者裝置。
訪問策略:主體和客體的對映規則,確定一個主體是否對客體具有訪問能力。
2、RBAC (Role-Base-Access-Controll)
基於角色的訪問控制(
資料來源
我的理解就是給使用者賦予不同的角色,給角色服務不同的許可權,許可權可以看成要訪問資源的集合,角色只是使用者和許可權的過渡層,可能會問為什要有一個角色 過渡層,可以試想一下沒有role過渡層,有多個使用者有著相同的數個許可權,分配管理許可權的時候,要重複分配許可權,工作量很大,更何況修改許可權的時候,更麻 煩,可以說不利於程式碼的維護,所以要有一個
二、TP中的RBAC類及使用RBAC類
1、RBAC許可權驗證的大致流程:
①、驗證當前操作是否需要驗證
②、驗證是否登入
③、檢視當前使用者的身份
④、獲取當前使用者的許可權列表
⑤、進行許可權驗證
2、RBAC所依賴的5張資料表
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;
CREATE TABLE `think_user` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(20) DEFAULT '',
`password` varchar(25) NOT NULL,
`phone_num` varchar(20) DEFAULT NULL,
`register_time` int(11) DEFAULT NULL,
`login_time` int(11) DEFAULT NULL,
`login_ip` varchar(20) DEFAULT NULL,
`status` tinyint(1) DEFAULT NULL,
PRIMARY KEY (`id`)) ENGINE=MyISAM DEFAULT CHARSET=utf8;
資料表模型如下:
這五張表的關係如下:
一個使用者對應著多個角色;一個角色可以屬於對個使用者;是多對多的關係,需要用箇中間表即role_user表;
一個角色可以有多個許可權;一個許可權可以屬於多個使用者;是多對多的關係,需要有個中間表即access表;
那node表示什麼?
node表是記錄許可權的表,說白了就是記錄應用,控制器,及方法的表,即要訪問資源的集合;比如要訪問的Admin應用下的Index控制器下的index方法;從這裡可以明確了TP的RBAC是控制使用者對控制器及方法的訪問許可權進行許可權的管理。下面來看個圖,加深理解:
這張圖裡面的使用者,角色,結點,許可權及使用者與角色中間表一一對應即可(這圖是從網上盜的)。其實還可將使用者進行抽象,對其進行分組,不同組別裡面的角色不同,這可一根據不同的業務進行嘗試。可以參考這篇部落格
知道了RBAC需要的5張資料表,下面來看看RBAC類是如何實現許可權驗證的。
3、RBAC類
RBAC類中三個核心的方法:
①getAccessList()獲取許可權列表
static public function getAccessList($authId) {
// Db方式許可權資料
$db = Db::getInstance(C('RBAC_DB_DSN'));
$table = array('role'=>C('RBAC_ROLE_TABLE'),'user'=>C('RBAC_USER_TABLE'),'access'=>C('RBAC_ACCESS_TABLE'),'node'=>C('RBAC_NODE_TABLE'));
$sql = "select node.id,node.name from ".
$table['role']." as role,".
$table['user']." as user,".
$table['access']." as access ,".
$table['node']." as node ".
"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";
$apps = $db->query($sql);
$access = array();
foreach($apps as $key=>$app) {
$appId = $app['id'];
$appName = $app['name'];
// 讀取專案的模組許可權
$access[strtoupper($appName)] = array();
$sql = "select node.id,node.name from ".
$table['role']." as role,".
$table['user']." as user,".
$table['access']." as access ,".
$table['node']." as node ".
"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";
$modules = $db->query($sql);
// 判斷是否存在公共模組的許可權
$publicAction = array();
foreach($modules as $key=>$module) {
$moduleId = $module['id'];
$moduleName = $module['name'];
if('PUBLIC'== strtoupper($moduleName)) {
$sql = "select node.id,node.name from ".
$table['role']." as role,".
$table['user']." as user,".
$table['access']." as access ,".
$table['node']." as node ".
"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";
$rs = $db->query($sql);
foreach ($rs as $a){
$publicAction[$a['name']] = $a['id'];
}
unset($modules[$key]);
break;
}
}
// 依次讀取模組的操作許可權
foreach($modules as $key=>$module) {
$moduleId = $module['id'];
$moduleName = $module['name'];
$sql = "select node.id,node.name from ".
$table['role']." as role,".
$table['user']." as user,".
$table['access']." as access ,".
$table['node']." as node ".
"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";
$rs = $db->query($sql);
$action = array();
foreach ($rs as $a){
$action[$a['name']] = $a['id'];
}
// 和公共模組的操作許可權合併
$action += $publicAction;
$access[strtoupper($appName)][strtoupper($moduleName)] = array_change_key_case($action,CASE_UPPER);
}
}
return $access;
}
可以看出方法是依次獲取專案模組,控制器,動作方法的許可權的。
②saveAccessList()檢測使用者的許可權列表,並將許可權儲存到SESSION中
//用於檢測使用者許可權的方法,並儲存到Session中
static function saveAccessList($authId=null) {
if(null===$authId) $authId = $_SESSION[C('USER_AUTH_KEY')];
// 如果使用普通許可權模式,儲存當前使用者的訪問許可權列表
// 對管理員開發所有許可權
if(C('USER_AUTH_TYPE') !=2 && !$_SESSION[C('ADMIN_AUTH_KEY')] )
$_SESSION['_ACCESS_LIST'] = self::getAccessList($authId);
return ;
}
③AccessDecision()許可權決策,就是判斷使用者擁有哪些許可權//許可權認證的過濾器方法
static public function AccessDecision($appName=MODULE_NAME) {
//檢查是否需要認證
if(self::checkAccess()) {
//存在認證識別號,則進行進一步的訪問決策
$accessGuid = md5($appName.CONTROLLER_NAME.ACTION_NAME);
if(empty($_SESSION[C('ADMIN_AUTH_KEY')])) {
if(C('USER_AUTH_TYPE')==2) {
//加強驗證和即時驗證模式 更加安全 後臺許可權修改可以即時生效
//通過資料庫進行訪問檢查
$accessList = self::getAccessList($_SESSION[C('USER_AUTH_KEY')]);
}else {
// 如果是管理員或者當前操作已經認證過,無需再次認證
if( $_SESSION[$accessGuid]) {
return true;
}
//登入驗證模式,比較登入後儲存的許可權訪問列表
$accessList = $_SESSION['_ACCESS_LIST'];
}
//判斷是否為元件化模式,如果是,驗證其全模組名
if(!isset($accessList[strtoupper($appName)][strtoupper(CONTROLLER_NAME)][strtoupper(ACTION_NAME)])) {
$_SESSION[$accessGuid] = false;
return false;
}
else {
$_SESSION[$accessGuid] = true;
}
}else{
//管理員無需認證
return true;
}
}
return true;
}
從程式碼中可以看出,許可權判斷是有兩種方式一種是根據session中的許可權列表進行校驗,一種是每次都查詢資料庫進行校驗(下面配置引數說明的時候會說明)
array(3) {
["phone"] => string(11) "18792455613"
["uid"] => string(2) "13"
["_ACCESS_LIST"] => array(1) {
["ADMIN"] => array(5) {
["ARTICLE"] => array(0) {
}
["CATEGORY"] => array(3) {
["CATEGORYLIST"] => string(2) "15"
["ADDCATEGORY"] => string(2) "16"
["ALTERCATEGORY"] => string(2) "17"
}
["MEDIA"] => array(2) {
["IMAGE"] => string(2) "12"
["VIDEO"] => string(2) "13"
}
["INDEX"] => array(5) {
["USERLIST"] => string(1) "7"
["ADDUSER"] => string(1) "8"
["ADDROLE"] => string(1) "9"
["ADDNODE"] => string(2) "10"
["INDEX"] => string(2) "11"
}
["DATA"] => array(0) {
}
}
}}
在AccessDecision()方法中呼叫checkAccess()方法進行驗證模組的過濾即去除不需要驗證的模組,控制器和方法。知道了RBAC的實現思路,下面來使用RBAC類進行許可權驗證。
4、使用RBAC類進行許可權驗證
①首先處理5張表,就是對5張表的增刪該查
實現邏輯程式碼如下:建立一個RbacController.class.php的控制器
/**
* 角色列表檢視
*/
public function role(){
$roleList = M("role")->select();
$this->assign("roleList",$roleList);
layout("base");
$this->display();
}
/**
* 角色表單處理
*/
public function roleHandle(){
$id = I("get.id");
if(M("role")->where(array("id"=>$id))->delete()){
$this->success("刪除成功",U("Admin/Index/role"));
}else{
$this->error("刪除失敗");
}
}
/**
* 結點列表檢視
*/
public function node(){
$field = array('id','name','title','pid');
$node = M('node')->field($field)->order('sort')->select();
$this->node = node_merge($node);
layout("base");
$this->display();
}
/**
* 許可權列表檢視
*/
public function access(){
$rid = I('rid',0,"intval");
$field = array('id','name','title','pid');
$node = M('node')->order('sort')->field($field)->select();
//原有的許可權
$access = M('access')->where(array('role_id'=>$rid))->getField('node_id',true);
$this->node = node_merge($node,$access);
// dump($this->node);
$this->rid = $rid;
layout("base");
$this->display();
}
/**
* 設定許可權表單處理
*/
public function setAccess(){
$rid = I('rid',0,"intval");
$db = M('access');
$db->where(array('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->success('修改成功',U('Admin/Index/role'));
}else{
$this->error('修改失敗');
}
}
/**
* 新增角色檢視
*/
public function addRole(){
layout("base");
$this->display();
}
/**
* 新增角色表單處理
*/
public function addRoleHandle(){
$data = I("post.");
if(M("role")->data($data)->add()){
$this->success("新增成功",U("Admin/Index/role"));
}else{
$this->error("新增角色失敗,請稍後再試");
}
}
/**
* 新增結點檢視
*/
public function addNode(){
$this->pid = I("pid",0,"intval");
$this->level = I("level",1,"intval");
switch ($this->level){
case 1:
$this->type = "應用";
break;
case 2:
$this->type = "控制器";
break;
case 3:
$this->type = "方法";
break;
}
layout("base");
$this->display();
}
/**
* 新增結點表單處理
*/
public function addNodeHandle(){
$data = I("post.");
if(M("node")->data($data)->add()){
$this->success("新增成功",U("Admin/Index/node"));
}else{
$this->error("新增失敗,請稍後再試");
}
}
其中有一個結點和許可權整合的函式node_merge(),實現的思路是遞迴進行結點新增,將統一控制器中的不同方法整合在一起,程式碼如下:這段程式碼放在Common資料夾下的function.php裡
/**
* 許可權結點整合
* @param array $node 結點陣列
* @param array $access 許可權陣列
* @param integer $pid 父級ID
* @return array 組合後的陣列
*/function node_merge($node,$access=null,$pid=0){
$arr = array();
foreach($node as $v){
if(is_array($access)){
$v['access'] = in_array($v['id'],$access) ? 1 : 0;
}
if($pid==$v['pid']){
$v['child'] = node_merge($node,$access,$v['id']);
$arr[] = $v;
}
}
return $arr;}
②完成了準備工作可以使用RBAC類了,首先先對RABC進行引數配置,下面是配置引數
配置檔案增加設定
USER_AUTH_ON 是否需要認證
USER_AUTH_TYPE 認證型別 //1:代表著登入驗證2:代表著試試驗證
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 節點表名稱
其中值得注意的是:下面四塊
USER_AUTH_GATEWAY 認證閘道器 //不是必須的
RBAC_DB_DSN 資料庫連線DSN //不是必須的
RBAC_USER_TABLE 使用者表名稱 //這裡是使用者和角色的中間表
USER_AUTH_TYPE 認證型別 //1:代表著登入驗證2:代表著試試驗證
時時驗證是指不將許可權資訊存入session,而是每訪問一個資源,查詢資料庫進行許可權校驗;
登入驗證是指將許可權資訊存入session,而是每訪問一個資源,根據session中的資訊進行驗證;
可以看出時時驗證的安全性更高,但消耗的資源也多。可以看看上面AccessDecision的程式碼加深理解。
下面是配置檔案的程式碼:
<?phpreturn array(
'RBAC_SUPERADMIN'=>'admin',
'ADMIN_AUTH_KEY' =>'superAdmin', //超級管理員識別
'USER_AUTH_ON' =>true, //是否開啟許可權驗證
'USER_AUTH_TYPE' =>1, //驗證型別(1:登入時驗證2:時時驗證)
'USER_AUTH_KEY' =>'uid', //使用者驗證識別號
'NOT_AUTH_ACTION' =>'index', // 無需驗證的動作方法
'NOT_AUTH_MODULE' =>'', //無需驗證的控制器
'RBAC_ROLE_TABLE' =>'think_role',//角色表名稱
'RBAC_USER_TABLE' => 'think_role_user',//使用者與角色的中間表
'RBAC_ACCESS_TABLE' =>'think_access',//許可權表
'RBAC_NODE_TABLE' =>'think_node',//節點表
'URL_HTML_SUFFIX' =>'',
// 'SHOW_PAGE_TRACE' => true,);
③開始“真正”使用RABC類進行許可權驗證
闡述下我用到的兩個類:
LoginController.class.php處理使用者登入請求;
CommonController.class.php新增_initialize()函式,讓其他控制器繼承此類;這裡值得說明的是_initialize()函式,他會在呼叫其他方法之前執行,這也就是為什麼要在這個函式中進行許可權驗證;
One: 使用登入驗證方式:首先在登入的時候向session中寫入許可權LoginController.class.php
<?php/**
* Created by PhpStorm.
* Date: 2016/1/29
* Time: 12:37
*/namespace Admin\Controller;use Org\Util\Rbac;use Think\Controller;class LoginController extends Controller{
/**
* 登入首頁檢視
*/
public function index()
{
$this->display();
}
/**
* 登入表單處理
*/
public function loginHandle()
{
if (IS_POST) {
$userModel = M('user');
$where = array(
'phone_num' => I('post.phone_num'),
'password' => md5(I('post.password'))
);
$result = $userModel->where($where)->find();
if (!$result) {
$this->error("登陸失敗");
}else{
session("phone",I('post.phone_num'));
session("uid",$result['id']);
if($result['name']==C('RBAC_SUPERADMIN')){
session(C('ADMIN_AUTH_KEY'),true);
}
//將許可權寫入session
Rbac::saveAccessList();
$this->success('歡迎登陸',U('Admin/Index/index'));
}
}
}
/**
* 登出操作
*/
public function logout(){
if(session("phone")){
session("uid",null);
session("phone",null);
session(null);
}
$this->redirect("Login/index");
}}
其次,在_initialize()函式中進行許可權驗證:CommonController.class.php
<?php/**
* Created by PhpStorm.
* User: zhangpeng
* Date: 2016/2/1
* Time: 23:28
*/namespace Admin\Controller;use Org\Util\Rbac;use Think\Controller;/**
* Class CommonController
* @package Admin\Controller
* 使用者驗證使用者的狀態,實現使用者頁面保留的功能
*/class CommonController extends Controller{
/**
* 自動執行函式,tp中自帶
*/
public function _initialize(){
if(!session("uid")){
$this->redirect('Login/index');
}
if(C('USER_AUTH_ON')){
Rbac::AccessDecision()||$this->error("你沒有許可權");
}
}}
Two:時時驗證,只需要在_initialize()函式中新增驗證即可,程式碼不在贅述。
其中值得注意的是,我用的是TP3.2.3,此時TP支援名稱空間和類自動載入機制,所以不像老版本需要用import引入RBAC類才能使用。
三、總結
RBAC類短短的200多行程式碼實現了許可權控制的需求,感覺十分厲害。後續把我的demo程式碼上傳,與大家一起學習
作者: 小灰灰heart