ThinkChat🤖让你学习和工作更高效,注册即送10W Token,即刻开启你的AI之旅 广告
有一个网友问,有权限吗?我之前也说了,会加上权限。一直很忙,没有去添加。今天就来更新一下,如何在 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 的原理。 >