RBAC(Role-Based Access Controll)基于角色的通路控制
在 ThinkPHP3.2.3 中 RBAC 類位于 /ThinkPHP/Library/Org/Util/Rbac.class.php
一、基本原理和資料庫設計
在背景管理子產品中,每個使用者都屬于相應的角色組,例如使用者 admin 屬于超級管理者角色組,使用者 dee 屬于普通管理者角色組,使用者 jane 屬于銷售角色組,使用者 nicole 屬于财務角色組,每個角色組擁有的權限都不同。使用者和角色組屬于多對多的關系,即一個使用者可能屬于多個角色組,一個角色組有多個使用者。
所有子產品(例如 Home、Admin)、控制器(Controller)、方法(Action)都是節點,角色組是否能夠通路這些節點的資訊即是該角色組的權限資訊。角色組和節點也是多對多的關系,即一個角色組可以通路多個節點,多個角色組都有可以通路同一個節點。
即 Rbac 功能需要 5 張資料表:使用者表、角色表、使用者-角色中間表、節點表、角色-節點中間表(權限表)。在 Rbac.class.php 中系統已經給出了其中的 4 張表:角色表(role)、使用者-角色中間表(role_user)、節點表(node)、權限表(access):
![](https://img.laitimes.com/img/__Qf2AjLwojIjJCLyojI0JCLiAjM2EzLcd3LcJzLcJzdllmVldWYtl2Pml2ZuYWZkVGZhVjZ3UmMkV2NjNjY4EWO2EjNhJGN0ATYwYTOvwFO5IzN4gDNtUGall3LcVmdhNXLwRHdo9CXt92YucWbpRWdvx2Yx5yazF2Lc9CX6MHc0RHaiojIsJye.gif)
4張表資訊
需要自己建立一張使用者表:
CREATE TABLE `crm_user` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`username` char(20) NOT NULL DEFAULT '',
`password` char(32) NOT NULL DEFAULT '',
`logintime` int(10) unsigned NOT NULL,
`loginip` varchar(30) NOT NULL,
`lock` tinyint(1) unsigned NOT NULL DEFAULT '0',
PRIMARY KEY (`id`),
UNIQUE KEY `username` (`username`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8
複制
資料模型如下:
基本的原理是,在配置檔案中配置使用者登入的識别号,這個識别号是使用者的 id,在使用者進行登陸的時候把 id 存儲在 Session 中,同時根據 Session 儲存的識别号通過連表查詢擷取該使用者所屬角色所能通路的節點資訊并做判斷。
二、配置選項
在 Rbac.class.php 中給出了需要配置的資訊:
// 配置檔案增加設定
// USER_AUTH_ON 是否需要認證
// USER_AUTH_TYPE 認證類型
// 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 節點表名稱
複制
在子產品配置檔案 ./Application/Admin/Conf/config.php 中添加:
//Rbac配置
'RBAC_SUPERADMIN'=>'admin', //超級管理者名稱
'ADMIN_AUTH_KEY'=>'superadmin', //超級管理者識别,存放在Session中
'USER_AUTH_ON'=>true, //是否開啟權限認證
'USER_AUTH_TYPE'=>1, //驗證類型 1-登陸時驗證 2-實時驗證
'USER_AUTH_KEY'=>'uid', //存儲在session中的識别号
'NOT_AUTH_MODULE'=>'Index', //無需驗證的控制器
'NOT_AUTH_ACTION'=>'add_role_handle', //無需驗證的方法
'RBAC_ROLE_TABLE'=>'crm_role', //角色表名稱
'RBAC_USER_TABLE'=>'crm_role_user', //角色與使用者的中間表名稱(注意)
'RBAC_ACCESS_TABLE'=>'crm_access', //權限表名稱
'RBAC_NODE_TABLE'=>'crm_node', //節點表名稱
複制
三、RBAC 類源碼分析
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 ;
}
複制
在登陸時調用,首先判斷是否傳遞了使用者識别号的參數,如果沒有傳遞,就從 Session 中讀取(配置檔案中配置的使用者識别号)對應的值;
如果配置的驗證類型是登陸時驗證(不是實時驗證)同時該使用者不是配置的超級管理者(Session 中不包含超級管理者識别号)時,就将調用 getAccessList 方法擷取角色的權限。
getAccessList 方法
根據傳遞的使用者識别号參數,通過連表查詢(role、role_user、access、node)獲得并傳回該使用者所屬的角色組擁有的所有節點的權限 。
AccessDecision 方法
在 Common 控制器的 _iniatialize 方法中調用該方法。
如果目前通路的控制器和方法都不在不需要驗證的節點資訊(需要配置)中,那麼調用該方法。
該方法首先調用 checkAccess 方法通過判斷配置中是否開啟 USER_AUTH_ON 來檢查是否需要認證,如果開啟了 USER_AUTH_ON ,則根據配置中需要驗證和無需驗證的子產品的配置檢查目前操作是否需要認證。
如果通過了 checkAccess 方法,則判斷 Session 中由 saveAccessList 方法建立的_ACCESS_LIST 數組是否包含目前通路的子產品、控制器和方法。超級管理者不由該方法進行認證。
四、開發執行個體
需要開發以下功能,順序是:
①【添加角色 → 角色清單】 →
②【添加節點 → 節點清單】 →
③【權限清單 → 配置設定權限】 →
④【添加使用者 → 使用者清單 】 →
⑤【Rbac 配置】→
⑥【登陸】
在背景子產品建立 Rbac 控制器:./Application/Admin/Controller/Rbac.class.php
① 角色
方法:
//角色清單
public function role_list() {
$this->role = M('role')->select();
$this->display();
}
//添加角色
public function add_role() {
$this->display();
}
//添加角色表單處理
public function add_role_handle() {
if(M('role')->add($_POST)) {
$this->success('添加成功',U('role_list','',''));
} else {
$this->error('添加失敗');
}
}
複制
視圖:
添加角色(展示)./Application/Admin/View/Rbac_add_role.html
<!DOCTYPE html>
<html>
<head>
<title>添加角色</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="__PUBLIC__/Css/public.css"/>
</head>
<body>
<form action="{:U('add_role_handle','','')}" method="post">
<table class="table">
<tr>
<th colspan="2">添加角色:</th>
</tr>
<tr>
<td align="right">角色名稱:</td>
<td>
<input type="text" name="name" />
</td>
</tr>
<tr>
<td align="right">角色描述:</td>
<td>
<input type="text" name="remark" />
</td>
</tr>
<tr>
<td align="right">是否開啟:</td>
<td>
<input type="radio" name="status" value="1" checked = "checked" />開啟
<input type="radio" name="status" value="0" />關閉
</td>
</tr>
<tr>
<td colspan="2" align="center">
<input type="submit" value="儲存添加">
</td>
</tr>
</table>
</form>
</body>
</html>
複制
角色清單 ./Application/Admin/View/Rbac_role_list.html
<!DOCTYPE html>
<html>
<head>
<title>TODO supply a title</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="__PUBLIC__/Css/Public.css">
</head>
<body>
<table class="table">
<tr>
<th>ID</th>
<th>角色名稱</th>
<th>角色描述</th>
<th>開啟狀态</th>
<th>操作</th>
</tr>
<foreach name="role" item="v">
<tr>
<td>{$v.id}</td>
<td>{$v.name}</td>
<td>{$v.remark}</td>
<td>
<if condition="$v['status'] eq 1">開啟<else />關閉</if>
</td>
<td>
<a href="{:U('access',array('rid'=>$v['id']),'')}">配置權限</a>
</td>
</tr>
</foreach>
</table>
</body>
</html>
複制
② 節點
方法:
//節點清單
public function node_list() {
$field = array('id', 'name', 'title', 'pid');
$node = M('node')->field($field)->order('sort asc')->select();
$this->node = node_regroup($node);//p($this->node);die;
$this->display();
}
//添加節點
public function add_node() {
$this->pid = I('get.pid', 0, 'int');//如果沒有傳遞的pid參數,則預設為0
$this->level = I('get.level', 1, 'int');//如果沒有傳遞的level參數,則level是1,代表頂級(子產品)
switch($this->level) {
case 1:
$this->type = '子產品';
break;
case 2:
$this->type = '控制器';
break;
case 3:
$this->type = '方法';
break;
}
$this->display();
}
//添加節點表單處理
public function add_node_handle() {
if(M('node')->add($_POST)) {
$this->success('添加成功',U('node_list','',''));
} else {
$this->error('添加失敗');
}
}
複制
視圖:
添加節點 ./Application/Admin/View/Rbac_add_node.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
<link rel="stylesheet" href="__PUBLIC__/Css/public.css">
</head>
<body>
<form action="{:U('add_node_handle','','')}" method="post">
<table class="table">
<tr><th colspan="2">添加{$type}</th></tr>
<tr>
<td align="right">{$type}名稱:</td>
<td>
<input type="text" name="name" />
</td>
</tr>
<tr>
<td align="right">節點描述:</td>
<td>
<input type="text" name="title">
</td>
</tr>
<tr>
<td align="right">是否開啟:</td>
<td>
<input type="radio" name="status" value="1" checked="checked" />開啟
<input type="radio" name="status" value="0" />關閉
</td>
</tr>
<tr>
<td align="right">排序:</td>
<td>
<input type="text" name="sort" />
</td>
</tr>
<tr>
<td colspan="2" align="center">
<input type="hidden" name="pid" value="{$pid}" />
<input type="hidden" name="level" value="{$level}" />
<input type="submit" value="添加{$type}" />
</td>
</tr>
</table>
</form>
</body>
</html>
複制
預設情況下從背景左側欄目進行節點添加,添加的是子產品(例如 Home 子產品,Admin 子產品)
節點清單 ./Application/Admin/View/Rbac_node_list.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
<link rel="stylesheet" href="__PUBLIC__/Css/public.css">
</head>
<body>
<div id="wrap">
<a href="{:U('add_node','','')}">添加子產品</a>
<table class="table">
<foreach name="node" item="app">
<div class="app">
<p>
<strong>{$app.title}</strong>
<a href="{:U('add_node',array('pid'=>$app['id'],'level'=>2),'')}">
[添加控制器]
</a>
<a href="">[修改]</a>
<a href="">[删除]</a>
</p>
<foreach name="app.child" item="controller">
<dl>
<dt>
-
<strong>{$controller.title}</strong>
<a href="{:U('add_node',array('pid'=>$controller['id'],'level'=>3),'')}">
[添加方法]
</a>
<a href="">[修改]</a>
<a href="">[删除]</a>
</dt>
<foreach name="controller.child" item="method">
<div>
-
<strong>{$method.title}</strong>
<a href="">[修改]</a>
<a href="">[删除]</a>
</div>
</foreach>
</dl>
</foreach>
</div>
</foreach>
</table>
</div>
</body>
</html>
複制
此時可以通過 GET 傳遞 pid 和 level 添加控制器節點和方法節點,例如
在節點清單的方法中,需要用到遞歸重組節點資訊,把在資料庫 node 表中存儲的節點資訊按照層級(子產品-控制器-方法的的層級)重新組合,結構類似于:
Array
(
[0] => Array
(
[id] => 6
[name] => Home
[title] => 前台應用
[pid] => 0
[child] => Array
(
)
)
[1] => Array
(
[id] => 1
[name] => Admin
[title] => 背景應用
[pid] => 0
[child] => Array
(
[0] => Array
(
[id] => 3
[name] => Index
[title] => 背景首頁
[pid] => 1
[child] => Array
(
[0] => Array
(
[id] => 17
[name] => index
[title] => 背景首頁
[pid] => 3
[child] => Array
(
)
)
)
)
[1] => Array
(
[id] => 4
[name] => ArticleManage
[title] => 文章管理
[pid] => 1
[child] => Array
(
[0] => Array
(
[id] => 8
[name] => index
[title] => 文章清單
[pid] => 4
[child] => Array
(
)
)
)
)
[2] => Array
(
[id] => 5
[name] => Rbac
[title] => 權限管理
[pid] => 1
[child] => Array
(
[0] => Array
(
[id] => 15
[name] => user_list
[title] => 使用者清單
[pid] => 5
[child] => Array
(
)
)
[1] => Array
(
[id] => 14
[name] => add_node_handle
[title] => 添加節點表單處理
[pid] => 5
[child] => Array
(
)
)
[2] => Array
(
[id] => 13
[name] => add_node
[title] => 添加節點
[pid] => 5
[child] => Array
(
)
)
[3] => Array
(
[id] => 12
[name] => node_list
[title] => 節點清單
[pid] => 5
[child] => Array
(
)
)
[4] => Array
(
[id] => 11
[name] => add_role_handle
[title] => 添加角色表單處理
[pid] => 5
[child] => Array
(
)
)
[5] => Array
(
[id] => 10
[name] => add_role
[title] => 添加角色
[pid] => 5
[child] => Array
(
)
)
[6] => Array
(
[id] => 9
[name] => role_list
[title] => 角色清單
[pid] => 5
[child] => Array
(
)
)
)
)
)
)
)
複制
在 ./Application/Admin/Common/function.php 中建立方法 node_group
<?php
/*
* 遞歸重組節點資訊
* @param $node 要重組的節點數組
* @param $pid 父級ID
* @return
*/
function node_regroup($node, $pid = 0, $access = null) {
$arr = array();
foreach($node as $v) {
if(is_array($access)) {
$v['access'] = in_array($v['id'], $access) ? 1 : 0;//判斷是否已經擁有權限
}
if($v['pid'] == $pid) {
$v['child'] = node_regroup($node, $v['id'], $access);
$arr[] = $v;
}
}p($arr);
return $arr;
}
複制
③ 權限
方法:
//配置權限
public function access() {
$rid = I('get.rid', 0, 'int');//角色id
$field = array('id', 'name', 'title', 'pid');
$node = M('node')->field($field)->order('sort asc')->select();
$access = M('access')->where('role_id = '.$rid)->getField('node_id', true);//已經擁有的權限
$node = node_regroup($node, 0, $access); //遞歸節點
$this->rid = $rid;
$this->node = $node;
$this->display();
}
//權限配置的表單送出處理
public function access_handle() {
$rid = I('rid', 0, 'int');
$db = M('access');
$db->where('role_id = '.$rid)->delete();//删除原有權限
$data = array();
if(!empty($_POST['access'])) {
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('role_list','',''));
} else {
$this->error('配置設定權限失敗');
}
}
}
複制
視圖:
從角色清單每一欄後面的“配置權限”點選進入
./Application/Admin/View/Rbac_access
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
<link rel="stylesheet" href="__PUBLIC__/Css/public.css">
<link rel="stylesheet" href="__PUBLIC__/Css/node.css">
<script src="__PUBLIC__/Js/jquery-1.7.2.min.js"></script>
</head>
<body>
<div id="wrap">
<a id="return" href="{:U('role_list','','')}">傳回</a>
<form action="{:U('access_handle')}" method="post">
<table class="table">
<foreach name="node" item="app">
<div class="app">
<p>
<strong>{$app.title}</strong>
<input type="checkbox" name="access[]" value="{$app.id}_1" level="1" <if condition="$app['access'] eq 1">checked="checked"</if>>
</p>
<foreach name="app.child" item="controller">
<div class="app_child">
<dl class="controller">
<dt>
<strong>{$controller.title}</strong>
<input type="checkbox" name="access[]" value="{$controller.id}_2" level="2" <if condition="$controller['access'] eq 1">checked="checked"</if>>
</dt>
</dl>
<foreach name="controller.child" item="method">
<span class="method">
<strong>{$method.title}</strong>
<input type="checkbox" name="access[]" value="{$method.id}_3" level="3" <if condition="$method['access'] eq 1">checked="checked"</if>>
</span>
</foreach>
<div style="clear:both"></div>
</div>
</foreach>
</div>
</foreach>
</table>
<input type="submit" value="送出" style="display: block; margin:0 auto; cursor:pointer">
<input type="hidden" name="rid" value="{$rid}">
</form>
</div>
</body>
<script>
$(function(){
$('input[level=1]').click(function(){
var inputs = $(this).parents('.app').find('input');
$(this).prop('checked') == true ? inputs.prop('checked', true) : inputs.prop('checked', false);
});
$('input[level=2]').click(function(){
var inputs = $(this).parents('.app_child').find('input');
$(this).prop('checked') == true ? inputs.prop('checked', true) : inputs.prop('checked', false);
});
});
</script>
</html>
複制
④ 使用者
方法:
//添加使用者
function add_user() {
$this->role = M('role')->select();
$this->display();
}
//添加使用者的表單送出處理
public function add_user_handle() {
$user = array(
'username'=>I('post.username', ''),
'password'=>I('post.password','','md5'),
);
$uid = M('user')->add($user);
$rold = array();
if($uid) {
foreach($_POST['role_id'] as $v) {
$role[] = array(
'role_id'=>$v,
'user_id'=>$uid
);
}
M('role_user')->addAll($role);
$this->success('添加成功', U('user_list','',''));
} else {
$this->error('添加失敗');
}
}
//使用者清單
public function user_list() {
$this->user = D('UserRelation')->field('password', true)->relation(true)->select();
//P(D('UserRelation')->getLastSql());
//p($this->user);
//die;
$this->display();
}
複制
關聯模型
./Application/Admin/Model/UserRelationModel.class.php
<?php
/*
* 使用者與角色關聯模型
*/
namespace Admin\Model;
use Think\Model\RelationModel;
class UserRelationModel extends RelationModel{
//定義主表名稱
protected $tableName = 'user';
//定義關聯關系
protected $_link = array(
'role'=>array(
'mapping_type'=>self::MANY_TO_MANY,
'foreign_key'=>'user_id',//指定主表外鍵
'relation_key'=>'role_id',//指定關聯表外鍵
'relation_table'=>'crm_role_user',//指定中間表名稱
'mapping_fields'=>'id,name,remark'//讀取的字段
),
);
}
複制
通過使用關聯模型,使需要輸出至 user_list 頁面的數組以如下方式輸出:
Array
(
[0] => Array
(
[id] => 1
[username] => admin
[logintime] => 1454222361
[loginip] => 127.0.0.1
[lock] => 0
[role] => Array
(
)
)
[1] => Array
(
[id] => 2
[username] => dee
[logintime] => 1454140261
[loginip] => 127.0.0.1
[lock] => 0
[role] => Array
(
[0] => Array
(
[id] => 2
[name] => Editor
[remark] => 網站編輯
)
)
)
)
複制
模型查詢的 SQL 語句類似于:
SELECT b.id,name,remark FROM crm_role_user AS a, crm_role AS b WHERE a.role_id = b.id AND a. user_id='2'
複制
視圖:
添加使用者 ./Application/Admin/View/Rbac_add_user.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
<css file="__PUBLIC__/Css/public.css" />
<js file="__PUBLIC__/Js/jquery-1.7.2.min.js" />
<style>
.add-role{
display:inline-block;
width:100px;
height:26px;
line-height: 26px;
text-align: center;
border: 1px solid #ccc;
border-radius: 4px;
margin-left: 20px;
cursor:pointer;
}
</style>
</head>
<body>
<form action="{:U('add_user_handle','','')}" method="post">
<table class="table">
<tr>
<th colspan="2">添加使用者</th>
</tr>
<tr>
<td align="right" width="40%">使用者賬号</td>
<td>
<input type="text" name="username">
</td>
</tr>
<tr>
<td align="right">密碼:</td>
<td>
<input type="password" name="password">
</td>
</tr>
<tr>
<td align="right">所屬角色:</td>
<td>
<select name="role_id[]" id="">
<option value="">請選擇角色</option>
<foreach name="role" item="v">
<option value="{$v.id}">{$v.name}({$v.remark})</option>
</foreach>
</select>
<span class="add-role">添加一個角色</span>
</td>
</tr>
<tr id="last">
<td colspan="2" align="center">
<input type="submit" value="儲存">
</td>
</tr>
</table>
</form>
</body>
<script>
$(function(){
$(".add-role").click(function(){
var obj = $(this).parents("tr").clone();
obj.find(".add-role").remove();
$("#last").before(obj);
});
});
</script>
</html>
複制
使用者清單 ./Application/Admin/View/Rbac_user_list.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
<link rel="stylesheet" href="__PUBLIC__/Css/public.css">
</head>
<body>
<table class="table">
<tr>
<th>ID</th>
<th>使用者名</th>
<th>上一次登陸時間</th>
<th>上一次登陸IP</th>
<th>使用者所屬組</th>
<th>是否鎖定</th>
<th>操作</th>
</tr>
<foreach name="user" item="v">
<tr>
<td>{$v.id}</td>
<td>{$v.username}</td>
<td>{$v.logintime|date="Y-m-d",###}</td>
<td>{$v.loginip}</td>
<td>
<if condition="$v['username'] eq C('RBAC_SUPERADMIN')">
超級管理者
<else/>
<ul>
<foreach name="v.role" item="value">
<li>{$value.name}({$value.remark})</li>
</foreach>
</ul>
</if>
</td>
<td>
<if condition="$v['lock'] eq 1">鎖定<else/>正常</if>
</td>
<td><a href="">鎖定</a></td>
</tr>
</foreach>
</table>
</body>
</html>
複制
④ 登陸和驗證
登陸:
在 ./Application/Admin/Controller/LoginController.class.php 的 login 方法中添加:
//判斷是否是超級管理者
if($_SESSION['user']['username'] == C('RBAC_SUPERADMIN')) {
session(c('ADMIN_AUTH_KEY'),true);
}
//讀取使用者權限
RBAC::saveAccessList();
複制
在 Common 控制器 (./Application/Admin/Controller/Common.class.php)的 _initialize 方法中添加驗證:
$notAuth = in_array(CONTROLLER_NAME,explode(',',C('NOT_AUTH_MODULE')))
|| in_array(ACTION_NAME,C('NOT_AUTH_ACTION'));
if(C('USER_AUTH_TYPE') && !$notAuth) {
//var_dump(Rbac::AccessDecision());
Rbac::AccessDecision() || $this->error('沒有通路權限',U('Admin/Index/index'));
}
複制
Index 控制器中的登陸 longin、退出 loginout 等方法不需要權限認證,可以把 Index 控制器加入到無需認證的控制器中,一些表單送出處理的方法可以加入到無需認證的方法中。