有一个网友问,有权限吗?我之前也说了,会加上权限。一直很忙,没有去添加。今天就来更新一下,如何在 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 次的才可以。此时我们看看,用户是否有添加用户的权限
![](https://box.kancloud.cn/9ac0e0d8f5c07c51e84e2e3e758162ef_1669x348.jpg)
这种页面中的按钮权限,是传统 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 的原理。
>