RBAC 是一套成熟的权限模型,元素包括:用户、角色、权限。在 RBAC 模型中,权限与角色相关联,用户通过成为对应角色的成员,从而得到这些角色的权限。即用户关联角色,角色关联权限,可实现系统权限的灵活配置。在本篇文章介绍到的后台管理系统中,角色-用户是一对多的关系。
在本项目中前端框架使用的是 adminLTE,可以在本项目目录文件的 public 文件夹下找到。
数据库是 MySQL,一共有 6 张数据表(本来是 4 张的,后边加了个文章管理,多了个文章和文章分类表)
1. 登录/退出模块
主要实现了登录和退出功能还有非法访问的功能。
由于是一个后台管理系统,所以没有涉及注册模块。作为一个登陆页面,而且是一个后台管理的登录页面,那么就不必那么的花哨,尽量的简洁美观就行,你可以给它起一个好听的名字,“xxx 后台管理”之类的。
接下来就是它需要实现的功能了,登录页面的逻辑是很简单的:
前端要做的就是给用户一个登录登录框,其中有用户名和密码以及验证码(注意:这里只是我想要的是让用户是用户名密码登录,你可以去设计其他的,可以是邮箱呀之类的等等),在前端的验证码这块儿使用的是 ThinkPHP 的一个扩展框架,topthink/think-captcha
,你可以直接在你的项目目录下使用 composer 命令直接去将它加载进来。在密码和用户名还有验证码验证这块儿是用的jquery-validation
,后边涉及到的表单验证都是使用的它,对于验证消息,我使用了 layui 的弹出层组件layer
,后边的所有的提示消息,警告消息都是使用的layer
。
验证码这块儿是需要注意的,如果你想要去重新定义或者说是设置你的验证码,你可以在 config 目录下的 captcha.php 中去添加一个额外的验证码信息,然后在 Login 控制器下重新定义一个公共方法,让它去完成重新生成验证码的功能。如果你像我一样重新定义了验证码的话,那么你就会发现重新刷新页面后,你的验证码点击之后是无法进行刷新的,惊不惊喜,意不意外。这时候,你先不要着急,可以先不去用自定义的验证码,回到原来的验证码,看看点击之后页面之中会有什么样的变化,你会发现它的查询字符串是会变化的,它后边多了个 id,这时候是不是就醒悟了呢,你可以给这张验证码图片添加一个点击事件,点击之后重新的设置它的 src 属性的值就行。这样验证码的问题就解决了。
后端要做的就是将用户输入的信息获取到,然后再数据库中查找信息,找到之后去判断用户名和密码是否匹配,如果不匹配,就需要返回一个错误信息了,这个错误信息最好是一个模糊信息,最好就是”用户名或密码不正确” 而不是指出哪一个不正确。如果匹配的结果是成功的,那么你就需要将当前的到的用户名和数据表中查询到的用户 id 放到 Session 中了,tp6 中默认是关着的,你需要在全局中间件定义文件 middleware.php 中将它开启,开启之后你就可以使用 Session 了,因为在非法访问功能中会用到当前登录的用户信息。
我们还需要一个中间控制器,这个中间控制器继承自 BaseController,这个中间控制器的作用就是非法访问(没有登陆成功的不允许访问,直接访问非登录页面强制跳转到登陆页面)登录之后的权限认证(如果当前用户不具有这个功能的权限,会直接给个弹窗,然后返回到上一个页面),在登录之后才能访问的页面都要继承自这个中间控制器,这样就做到了非法访问的功能。
2.主页面模块
主页面的前端页面我用的是 adminLTE 中的 index.html 来修改的,它需要做到的效果就是能够根据不同的用户登录,然后自动去渲染菜单栏,还有用户的退出登录和清除缓存的功能。
自动渲染无限级菜单:这个就需要去数据库中获取对应角色的权限了,然后再根据角色对用户进行权限绑定,然后还需要一个无限级菜单的划分,这样对于数据的处理就可以传到前端进行渲染了。最难的部分应该就是无限级菜单的划分了,这个就得根据数据表中的数据是怎样的来设计处理函数了,我的菜单数据表中的有一个 pid 字段,它代表着当前菜单的上级菜单的 id 值,就可以对当前用户所具有的权限来进行查询,获取到他的权限字段的数据,然后再对这些权限根据菜单表中的 pid 字段进行处理,就可以得到一个想要的数据了。
// 无限级分类--循环一级导航和子导航,将结果组合成一个多维数组
public static function ruleLayer($rule, $pid=0): array
{
// 声明一个空数组,用来存放生成的多维数组
$rules = array();
foreach ($rule as $vl){
// 判断当前的菜单是否是主菜单
if ($vl['pid'] == $pid){
// 如果是主菜单,就给它添加一个child字段,然后将所有pid===它的id的所有菜单赋给这个child字段
$vl['child'] = static::ruleLayer($rule, $vl['id']);
$rules[] = $vl;
}
}
return $rules;
}
前端渲染就可以使用 tp6 自己封装的 volist,然后进行二级菜单的渲染。
退出登录:这是一个很简单的逻辑实现,点击退出登录后,在后端将 session 中的数据清空,然后再将页面重新定向到登陆页面就可以了,可以使用 redirect()函数进行操作。
清除缓存:清除缓存,在 tp6 中就是清除掉 runtime 目录下的当前应用所产生的日志文件,还有一些缓存数据,这个操作就有些麻烦了,因为 rmdir()函数它只能删除一个空的文件夹,也就是说,想要删除一个文件夹,你得将这个文件夹下的所有内容全部的删掉,首先要获取到当前的应用的运行目录,可以直接使用 tp6 封装的助手函数来获取,app()->getRuntimePath();
,然后再对当前文件夹下的内容进行删除,具体函数实现如下.
function delete_dir_file($dir)
{
// 声明一个初始状态,默认情况下缓存未被删除
$res = false;
// 检验一个目录是否真实
if (is_dir($dir)){
// 成功打开目录流,返回值是一个resource类型数据,如果不成功,返回false
if ($handle = opendir($dir)){
while (($file = readdir($handle)) != false){
// echo "filename: " . $file . "<br>";
/*
* filename: . 代表当前访问目录存在同级目录
* filename: .. 代表存在上级目录
* filename: log 子目录
* filename: session 子目录
* filename: temp 子目录
*/
if ($file !== '.' && $file !== '..'){
// 判断是否是一个目录
if (is_dir($dir . '\\' . $file)){
// 拼接目录
// 目录只有为空的情况下才能删除
delete_dir_file($dir . '\\' . $file);
}else{
// 不是目录的情况,直接删除
// unlink只能删除一个文件
unlink($dir . '\\' . $file);
}
}
}
}
// 关闭目录句柄
closedir($handle);
// 目录为空时删除目录
if ($dir !== "E:\\Visual Studio Code\\harbor\\tp\\runtime\\admin\\session\\"){
if (rmdir($dir)){
$res = true;
}
}
}
return $res;
}
在主页面还可以将当前登录的用户信息也渲染出来,获取到当前 session 中的值,然后渲染到页面中.
3.用户管理模块
用户管理模块的数据显示这里我使用了 DataTables 表格插件,它是一款 jquery 表格插件,可以在DataTables 中文网查看它的使用教程。
用户管理模块主要是实现了用户的新增,编辑,删除功能,对于角色是超级管理员的用户,设置了不允许修改自己的权限还有不允许创建一个新的超级管理员。
对于用户管理模块首页的数据渲染,由于只涉及到了一张表,所以只需要注意 DataTables 插件要求返回的数据就行了,然后从数据库中取出查到的数据,返回给前端页面来进行渲染,如果在 DataTables 中设置了分页和排序要求,那么在设计数据库查询语句的时候就必须要添加上。
用户新增:用户新增涉及到两张数据表,一张是用户表,一张是用户-角色关联表,首先在前端页面的显示时,需要将角色表中的角色信息全部都获取出来,然后将获取到的角色信息传给前端进行一个下拉列表的渲染,然后就是前端进行添加一个新的用户操作,然后将数据 post 给后端,后端接收到数据之后,需要对用户名进行查重判断,如果不重复,先更新用户表,再更新用户-角色关联表,用户-角色关联表是在用户表更新成功的前提下进行更新的。如果重复,直接退出脚本,返回给前端一个信息,重定位到用户名那里。
用户编辑:用户编辑同样的涉及到了两张表,用户表和用户-角色关联表,编辑的时候需要获取到当前编辑的用户信息,然后渲染到页面,所以,可以通过 get 方法,将当前编辑的用户的 uid 传给后端,然后后端在数据库中查找记录,然后返回给前端进行渲染。更新的时候可新增一样,需要先对用户表进行更新,如果更新成功,再去对用户-角色关联表进行更新。
用户删除:也涉及到两张表,用户表和用户-角色关联表,再点击删除后,需要给用户一个提示信息,让他确定是否删除,然后再对删除方法进行调用,删除之后,给用户一个删除成功提示。
4.规则管理模块
规则管理是对菜单页面的显示,新增一级菜单,新增子菜单,编辑菜单还有删除菜单功能的实现。规则管理同样的使用了 DataTables 表格插件,不过对于上级菜单和子菜单如果需要进行效果展示的话,就需要对菜单进行无限级菜单处理,经过无限级菜单分类之后,就可以将数据传到前端进行渲染了。
// 无限级菜单分类, 前端规则列表分类
public static function RuleList($rule, $pid=0, $lev=1): array
{
$arr = array();
foreach ($rule as $r){
if ($r['pid'] == $pid){
$r['lev'] = $lev;
$arr[] = $r;
$arr = array_merge($arr, static::RuleList($rule, $r['id'], $lev+1));
}
}
return $arr;
}
新增菜单:新增菜单这块儿有两个选择,一个是新增一个一级菜单,一个是新增一个子菜单,新增子菜单的时候是点击每个菜单规则后边的按钮,新增一级子菜单的时候是点击最上边的新增按钮,两个按钮唯一不同的地方是在新增子菜单的地方,get 时候的参数不同,子菜单的 url 的查询语句中增加了 pid,然后就可以在后端接收的时候进行检查,如果 get 到 pid 那就是新增一个子菜单,如果没有那就是新增一个一级菜单。
编辑菜单:编辑菜单需要获取到当前菜单的 id,然后在数据库中查找到当前菜单的信息,返回前端进行渲染,然后用户修改当前规则信息,返回数据给后端,后端将修改后的信息更新到数据库中去。
删除菜单:删除菜单和删除角色的功能类似,不过是删除菜单只涉及到一张数据表,只需要对一张数据表中的数据进行删除就可以了,删除菜单首先要做的就是查询当前菜单下有没有子菜单,将 id 当作 pid 传入数据表中进行查询,如果存在,就不能进行删除。
5.角色管理模块
角色管理模块中最难的就是一个 checkbox 复选框的处理,角色模块包括了角色的删除,新增和编辑,删除角色和上边的思路是一样的,而编辑和添加就不一样了,编辑和添加需要做到的是,首先在前端页面中需要将所有的规则列举出来,要有层级感,在编辑的时候需要对当前角色所拥有的权限全部都选中。同样,在后端获取到所有的菜单信息时,要对数据进行无限级菜单的处理,然后再传入前端进行渲染。
6.数据库模块
本项目目前一共涉及到 6 张表,其中有 4 张表是现在用到的,另外两张表是我添加的扩展功能所设计的数据表,现在用到的表有: 1. 用户表“users”,2. 角色表“auth_role”,3. 规则表“auth_rule”, 4. 角色-用户关联表“users_role”.
本项目的所有源码都可在 Git 上查看。