有一個網友問,有權限嗎?我之前也說了,會加上權限。一直很忙,沒有去添加。今天就來更新一下,如何在 thinkphp5 框架中實現權限的控制。
主流的權限控制有 rbac 和 auth 。我先來介紹一下 rbac,然后在此基礎之上,再介紹一下 auth 的實現。
rbac 這個系統的設計,就拿我之前開源的系統來講解吧。正好,那個系統實現的就是 rbac 。你可以到這個地址[github 上的 snake](https://github.com/nick-bai/snake),或者直接到源碼下載那一章節下載我演示的代碼。
rbac 中文名 叫 角色權限控制,具體的什么解釋,你可以自行百度,比我解釋的專業。這里,我只是想說明一下,設計 rabc 權限的思路。首先我們的系統必須擁有的表有如下幾張:
1、用戶表
這個是必須的,因為系統需要用戶的登錄,這是不可或缺的。
2、節點表
記錄著系統中的各個操作節點,方便我們通過這些節點去拼裝菜單,以及權限的分配。
> 所謂的節點,在 rbac 中你可以理解成:模塊、控制器、方法。這些對應的名字。
3、角色表
存放各種系統的角色。
這幾張表有了。講一下,具體的實現方式。
> 我們 通過 給角色分配一些操作節點的權限,然后再給 用戶 指定角色。這樣,當用戶當用戶操作某個 控制器\\方法 的時候,我們檢測他所屬的角色,是否有這個 節點的規則,就能判斷,他是否可以操作這個 方法。
講到 auth 可以理解成加強版的 rbac,他不僅可以驗證節點,同樣還可以比 rbac 驗證更多的小細節。比如,某個節點,必須要 積分 > 500 的才能操作,因為 rabc 只能控制節點(這個節點就是由 模塊\\控制器\\方法名 組成的字串),無法驗證別的小細節。而 auth 是驗證 規則的,而不是節點。
> 遺憾的是,目前能找到的一些介紹 auth 的 thinkphp 代碼,其實就是 rbac,并沒有展示 auth 比 rbac 好的地方。另外,其實你做一個 auth 權限系統,也并不一定要官方的那個 auth 類,這個類 thinkphp5 官方暫時未提供。其實你要是理解原理之后,很容易寫出和你的系統化完全匹配的 auth 方法來。
在我們做 rbac 的時候,錄入的節點是按照 模塊、控制器、方法名,這樣的順序錄入到 節點表中的,而在我們驗證權限的時候,又得將這些 模塊、控制器、方法名拼接成字符串。
比如:我們在表中錄入 index 、shop、addShop 這三個節點,而我們在 驗證的時候,會驗證 index/shop/addshop,這樣去驗證,而我拼裝成的這個樣子的 字串**index/shop/addshop**就是 auth 中所講的 規則。其實,我們在 rbac 權限系統中,去分開錄入 這樣三個 字段,不如直接錄入像 auth 這樣的規則。反而更利于我們的后續操作。
說到這里,有沒有發現,其實 rbac 和 auth 是很像的,這也就是為什么,很多的所謂的講 auth 的代碼,都是“掛羊頭賣狗肉”的rbac。其實,我們只要在 節點表 (auth 中稱為規則表)中,加入一個 附件條件 字段,在驗證規則的同是,去檢測 附加條件 是否滿足,從而實現更加細節的驗證。至于這個附加條件,你怎么去設計。你可以按照官方的那種方式去設計,也可以自己去 設計這個填寫格式,反正你自己能有辦法解析就行。
我們在正式開始寫 auth 系統之前,先來看看,我們需要設計哪些表。
1、用戶表
~~~
-- ----------------------------
-- Table structure for auth_user
-- ----------------------------
DROP TABLE IF EXISTS `auth_user`;
CREATE TABLE `auth_user` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`username` varchar(255) COLLATE utf8_bin DEFAULT '' COMMENT '用戶名',
`password` varchar(255) COLLATE utf8_bin DEFAULT '' COMMENT '密碼',
`loginnum` int(11) DEFAULT '0' COMMENT '登陸次數',
`last_login_ip` varchar(255) COLLATE utf8_bin DEFAULT '' COMMENT '最后登錄IP',
`last_login_time` int(11) DEFAULT '0' COMMENT '最后登錄時間',
`real_name` varchar(255) COLLATE utf8_bin DEFAULT '' COMMENT '真實姓名',
`status` int(1) DEFAULT '0' COMMENT '狀態',
`roleid` int(11) DEFAULT '1' COMMENT '用戶角色id',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8 COLLATE=utf8_bin;
-- ----------------------------
-- Records of auth_user
-- ----------------------------
INSERT INTO `auth_user` VALUES ('1', 'admin', '21232f297a57a5a743894a0e4a801fc3', '32', '127.0.0.1', '1490852367', 'admin', '1', '1');
INSERT INTO `auth_user` VALUES ('2', 'xiaobai', '4297f44b13955235245b2497399d7a93', '6', '127.0.0.1', '1470368260', '小白', '1', '2');
~~~
2、角色表 (在 auth 中稱之為 權限組 其實是一個概念)
~~~
-- ----------------------------
-- Table structure for auth_role
-- ----------------------------
DROP TABLE IF EXISTS `auth_role`;
CREATE TABLE `auth_role` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT 'id',
`rolename` varchar(155) NOT NULL COMMENT '角色名稱',
`rule` varchar(255) DEFAULT '' COMMENT '權限節點數據',
PRIMARY KEY (`id`)
) ENGINE=MyISAM AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;
-- ----------------------------
-- Records of auth_role
-- ----------------------------
INSERT INTO `auth_role` VALUES ('1', '超級管理員', '');
INSERT INTO `auth_role` VALUES ('2', '系統維護員', '1,2,3,4,5,6,7,8,9,10');
INSERT INTO `auth_role` VALUES ('3', '新聞發布員', '1,2,3,4,5');
~~~
3、規則表
~~~
-- ----------------------------
-- Table structure for auth_node
-- ----------------------------
DROP TABLE IF EXISTS `auth_node`;
CREATE TABLE `auth_node` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`node_name` varchar(155) NOT NULL DEFAULT '' COMMENT '節點名稱',
`rule` varchar(155) NOT NULL COMMENT '權限規則',
`is_menu` tinyint(1) NOT NULL DEFAULT '1' COMMENT '是否是菜單項 1不是 2是',
`typeid` int(11) NOT NULL COMMENT '父級節點id',
`style` varchar(155) DEFAULT '' COMMENT '菜單樣式',
`condition` varchar(155) DEFAULT NULL COMMENT '附加條件',
PRIMARY KEY (`id`)
) ENGINE=MyISAM AUTO_INCREMENT=15 DEFAULT CHARSET=utf8;
-- ----------------------------
-- Records of auth_node
-- ----------------------------
INSERT INTO `auth_node` VALUES ('1', '用戶管理', '#', '2', '0', 'fa fa-users', null);
INSERT INTO `auth_node` VALUES ('2', '用戶列表', 'user/index', '2', '1', '', null);
INSERT INTO `auth_node` VALUES ('3', '添加用戶', 'user/useradd', '1', '2', '', null);
INSERT INTO `auth_node` VALUES ('4', '編輯用戶', 'user/useredit', '1', '2', '', null);
INSERT INTO `auth_node` VALUES ('5', '刪除用戶', 'user/userdel', '1', '2', '', null);
INSERT INTO `auth_node` VALUES ('6', '角色列表', 'role/index', '2', '1', '', null);
INSERT INTO `auth_node` VALUES ('7', '添加角色', 'role/roleadd', '1', '6', '', null);
INSERT INTO `auth_node` VALUES ('8', '編輯角色', 'role/roleedit', '1', '6', '', null);
INSERT INTO `auth_node` VALUES ('9', '刪除角色', 'role/roledel', '1', '6', '', null);
INSERT INTO `auth_node` VALUES ('10', '分配權限', 'role/giveaccess', '1', '6', '', null);
INSERT INTO `auth_node` VALUES ('11', '系統管理', '#', '2', '0', 'fa fa-desktop', null);
INSERT INTO `auth_node` VALUES ('12', '數據備份/還原', 'data/index', '2', '11', '', null);
INSERT INTO `auth_node` VALUES ('13', '備份數據', 'data/importdata', '1', '12', '', null);
INSERT INTO `auth_node` VALUES ('14', '還原數據', 'data/backdata', '1', '12', '', null);
~~~
> 關于系統的管理員登錄、角色的增刪改查、規則的增刪改查、用戶的增刪改查等,這些基礎的功能,在本部分不做過多的介紹。相信你通過前面的,用戶的增刪改查,已經會用 thinkphp5 完成 CURD 的操作了。本初只重點介紹,權限系統的具體起作用的部分。
## 從登陸開始講起
登錄的基礎功能,校驗用戶名,密碼,驗證碼這些內容,代碼中都有,此處不做過多的介紹。開始看看,我們在登陸的時候,應該做哪些權限的工作。
確認用戶一切信息正確之后,我做了如下的操作 Login.php
~~~
//獲取該管理員的角色信息
$user = new UserType();
$info = $user->getRoleInfo($hasUser['roleid']);
~~~
根據用戶的 角色id 去獲取用戶所擁有的 權限信息。我在用戶表中設置了一個 rule 的字段,這個字段以逗號隔開,存儲著用戶的權限節點的 id。例如 rule 字段的結果是 1,2 。 那么對應的節點就是 # 和 user/index 也就是擁有,用戶列表查看的權限。我們拿著用戶的權限 rule 去 node 表中把他擁有的 權限節點數據全部查出。 Usertype.php
~~~
/**
* 獲取角色信息
* @param $id
*/
public function getRoleInfo($id){
$result = db('role')->where('id', $id)->find();
if(empty($result['rule'])){
$where = '';
}else{
$where = 'id in('.$result['rule'].')';
}
$res = db('node')->field('rule')->where($where)->select();
foreach($res as $key=>$vo){
if('#' != $vo['rule']){
$result['action'][] = $vo['rule'];
}
}
return $result;
}
~~~
> 我們在此處設計的是 超級管理員的 rule 是空,以此來標識他是超級管理員,超級管理員擁有全部的權限。
查詢出全部的節點,把這些節點,存儲到 session 中,這樣我們就不需要每次都去查取用戶的權限節點,提高效率。
~~~
session('username', $username);
session('id', $hasUser['id']);
session('role', $info['rolename']); //角色名
session('action', $info['action']); //角色權限
~~~
action 節點的數據如下:
~~~
Array
(
[0] => user/index
[1] => user/useradd
[2] => user/useredit
[3] => user/userdel
[4] => role/index
[5] => role/roleadd
[6] => role/roleedit
[7] => role/roledel
[8] => role/giveaccess
[9] => data/index
[10] => data/importdata
[11] => data/backdata
)
~~~
用戶擁有的節點,就放在這樣的數組里面。這樣,當用戶操作某一個節點時候,直接判斷所操作的節點是否在這個數組中即可。這些都是后面的話了,我們接著看,login 之后操作了哪些。
登錄成功之后,跳轉到 index/index 控制器,而 index 控制器,又繼承了 Base.php 這個基類,這個基類中,我們可以做一些全局的檢測。
## 權限檢測 Base.php
我們來看一下,Base.php 做了哪些操作
~~~
public function _initialize()
{
if(empty(session('username'))){
$this->redirect(url('login/index'));
}
//檢測權限
$canDo = authCheck();
if(!$canDo){
$this->error('沒有權限');
}
//獲取權限菜單
$node = new Node();
$this->assign([
'username' => session('username'),
'menu' => $node->getMenu(session('rule')),
'rolename' => session('role')
]);
}
~~~
用戶未登錄,跳轉到登錄。如果用戶登錄成功了,此時我們進行權限的檢測。auCheck(),定義在 common.php 中
~~~
function authCheck(){
$control = lcfirst(request()->controller());
$action = lcfirst(request()->action());
//跳過登錄系列的檢測以及主頁權限
if(!in_array($control, ['login', 'index'])){
if(!in_array($control . '/' . $action, session('action'))){
return false;
}
}
return true;
}
~~~
此處我們只是做了節點的驗證,你可以理解成目前還是 rbac 也就是市面上絕大多數的 所謂的 rbac 就只是檢測到這一步。很簡單,拼接現在的操作節點字串,是否在該用戶所在的權限數組中就可以了。不在,提示無權限。
最后,比較主要的步驟,根據用戶的權限節點,拼接出用戶擁有的 左側操作菜單。getMenu()
~~~
/**
* 根據節點數據獲取對應的菜單
* @param $nodeStr
*/
public function getMenu($nodeStr = '')
{
//超級管理員沒有節點數組
$where = empty($nodeStr) ? 'is_menu = 2' : 'is_menu = 2 and id in('.$nodeStr.')';
$result = db('node')->field('id,node_name,typeid,rule,style')
->where($where)->select();
$menu = prepareMenu($result);
return $menu;
}
~~~
這里又調用了 定義在 common.php 中的 prepareMenu() 方法
~~~
/**
* 整理菜單住方法
* @param $param
* @return array
*/
function prepareMenu($param)
{
$parent = []; //父類
$child = []; //子類
foreach($param as $key=>$vo){
if($vo['typeid'] == 0){
$vo['href'] = '#';
$parent[] = $vo;
}else{
$vo['href'] = url($vo['rule']); //跳轉地址
$child[] = $vo;
}
}
foreach($parent as $key=>$vo){
foreach($child as $k=>$v){
if($v['typeid'] == $vo['id']){
$parent[$key]['child'][] = $v;
}
}
}
unset($child);
return $parent;
}
~~~
我們只要在頁面中,對應的位置,渲染出這個整理更好的菜單,就可以完成操作欄,根據不同的權限,顯示不同的菜單了。
至此, rbac 部分算是結束了。
上一章的結尾,我說的是至此,rbac 的部分結束了。有人可能感覺很奇怪,不是說講的是 auth 嗎,怎么又 rbac 了。其實,你可以把 auth 理解成 rbac 的加強版。一個 auth 權限系統,首先要有 rbac 的功能。接下來才是,其區別于 rbac 的重點所在。也是 絕大部分所謂的 auth 權限系統未提及的部分。
> 我在這個文檔中講解的 auth 權限,并沒有用到 thinkphp 3.2 中給到的 auth 類,如果你想找通過改 3.2 那個類而來的 auth 權限系統。那你可能要失望了,不是我不會改那個類,而是我覺得,你懂了原理之后,根本沒必要拘泥于那個類,完全可以自己定義。
**如何正我們的 rbac 基礎之上,改成 auth 呢?**
auth 區別于 rabc 的主要點是,auth 檢測的是規則,而規則我們已經有了,那就是 node 表中的 rule 字段,其實就是節點的標識。另外一點, auth 系統中通常會在 節點后面加一個 condition 字段,以此來標識,想要擁有這個權限,你還應該有哪些額外的條件。而這個條件的填寫和解析是最為關鍵的點。
## 開始修改
首先,我們要制定一個額外的條件規則,本處為了解析的方便,以及展示原理的原則,我設計一個簡單的條件規則
~~~
user|id={uid} and loginnum > 20
~~~
條件牽扯的表|條件字段=當前用戶id and 條件字段 > 20
這個語句的意思就是 某個權限的需要滿足這個用戶在 user 表中登錄的次數大于 20 才能有權限。
從之前的我展示的數據可以看到,管理員的登錄次數是 30多次。那我們就以這個例子進行講解。首先在 添加用戶 這個權限字段,也就是 node 表中的第三條 添加一個 condition 字段值 user|id={uid} and loginnum>200,也就是規定,useradd 操作的額外權限是 操作次數必須大于 200 次的才可以。此時我們看看,用戶是否有添加用戶的權限

這種頁面中的按鈕權限,是傳統 rbac 很那去控制的。可見此時,用戶有添加 用戶的權限。我們修改一下權限檢測方法 authCheck
~~~
function authCheck($condition=false, $url=''){
$control = lcfirst(request()->controller());
$action = lcfirst(request()->action());
if(empty($condition)){
$checkUrl = $control . '/' . $action;
}else{
$checkUrl = $url;
if(empty($checkUrl)) return false;
// 檢測附加條件
$condition = db('node')->field('condition')->where("rule = '" . $checkUrl . "'")->find();
// 解析附加添加 形如:user|id={uid} and loginnum > 20
if(empty($condition)){
return true;
}
$rule = explode("|", $condition['condition']);
unset($condition);
$table = $rule['0'];
$where = str_replace("{uid}", session('id'), $rule['1']);
$can = db($table)->where($where)->find();
if(empty($can)) return false;
}
if(!in_array($control, ['login', 'index'])){
if(!in_array($checkUrl, session('action'))){
return false;
}
}
return true;
}
~~~
這樣我們的簡單的 權限檢測 函數就完成了。當然這個函數還很弱,只能檢測某一種規則。如果你想檢測復雜的規則,你可以自己完善和定制更多的規則,原理就是你得會解析這些規則。就像這樣
~~~
$rule = explode("|", $condition['condition']);
unset($condition);
$table = $rule['0'];
$where = str_replace("{uid}", session('id'), $rule['1']);
~~~
當然,github上 官方已經寫好了一個類[https://github.com/yunwuxin/think-auth](https://github.com/yunwuxin/think-auth)后面我會講解這個的用法,當然這個就非常強大了,支持很多種認證。
## 如何去驗證
比如我們去驗證這個需要額外權限的 添加用戶 按鈕是否需要展示,在按鈕頁面
~~~
<div class="form-group clearfix col-sm-1">
{if(authCheck(true, 'user/useradd'))}
<a href="./userAdd"><button class="btn btn-outline btn-primary" type="button">添加用戶</button></a>
{/if}
</div>
這樣就能驗證,這個添加按鈕的額外權限了。
## 預告
后面我會研究 官方給的那個擴展,講解一個強大的 auth 權限,本次只是講解自己去實現 auth 的原理。
>