RBAC(Role-Based Access Controll)基於角色的訪問控制php
在 ThinkPHP3.2.3 中 RBAC 類位於 /ThinkPHP/Library/Org/Util/Rbac.class.phpcss
在後臺管理模塊中,每一個用戶都屬於相應的角色組,例如用戶 admin 屬於超級管理員角色組,用戶 dee 屬於普通管理員角色組,用戶 jane 屬於銷售角色組,用戶 nicole 屬於財務角色組,每一個角色組擁有的權限都不一樣。用戶和角色組屬於多對多的關係,即一個用戶可能屬於多個角色組,一個角色組有多個用戶。html
全部模塊(例如 Home、Admin)、控制器(Controller)、方法(Action)都是節點,角色組是否可以訪問這些節點的信息便是該角色組的權限信息。角色組和節點也是多對多的關係,即一個角色組能夠訪問多個節點,多個角色組都有能夠訪問同一個節點。node
即 Rbac 功能須要 5 張數據表:用戶表、角色表、用戶-角色中間表、節點表、角色-節點中間表(權限表)。在 Rbac.class.php 中系統已經給出了其中的 4 張表:角色表(role)、用戶-角色中間表(role_user)、節點表(node)、權限表(access):jquery
/* -- -------------------------------------------------------- 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 `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 保存的識別號經過連表查詢獲取該用戶所屬角色所能訪問的節點信息並作判斷。session
在 Rbac.class.php 中給出了須要配置的信息:app
// 配置文件增長設置 // 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', //節點表名稱
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 控制器加入到無需認證的控制器中,一些表單提交處理的方法能夠加入到無需認證的方法中。
注:後臺模板來自後盾網提供的免費下載