SpringBoot实现RBAC权限校验模型
最近想做一个管理系统,记录下自己的消费记录,实现个文件上传之类的功能,正好最近看了下RBAC模型,想学习一下,在这分享下实现过程,方便以后查看
RBAC模型简单来说就是创建5张表(用户表,角色表,权限表,角色与用户表,角色与权限表)
通过给用户分配不同角色,以及给角色分配不同权限,实现用户的权限控制
不过这次我做的项目是个后台管理系统,主要是自己和朋友使用,所以角色其实就分成了管理员和普通用户,因此我省略掉了角色和角色权限表,这样虽然不够灵活,但毕竟使用的人少,所以管理起来也非常方便
数据库表
数据库主要创建三张表
用户表:
主要用来使用用户登录
- id 用户主键
- account 登录账号
- password 登录密码
- type 区分普通角色和管理员身份
- username 用户名
- status 用户的状态
+-----------+-------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+-----------+-------------+------+-----+---------+----------------+
| id | int | NO | PRI | NULL | auto_increment |
| account | char(15) | YES | | NULL | |
| password | varchar(50) | YES | | NULL | |
| type | int | YES | | NULL | |
| username | varchar(20) | YES | | NULL | |
| status | int | YES | | NULL | |
+-----------+-------------+------+-----+---------+----------------+
权限表:
主要用来记录权限信息
- id 权限主键
- name 权限名称
- parent_id 父权限的id 如果没有父权限则为null
- status 权限是否启用
- remark 权限备注
- level 权限的层级,主要用来排序的字段
+-------------+-------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+-------------+-------------+------+-----+---------+----------------+
| id | int | NO | PRI | NULL | auto_increment |
| name | varchar(20) | YES | | NULL | |
| parent_id | int | YES | | NULL | |
| status | int | YES | | NULL | |
| remark | varchar(50) | YES | | NULL | |
| create_user | int | YES | | NULL | |
| create_date | date | YES | | NULL | |
| update_user | int | YES | | NULL | |
| update_date | date | YES | | NULL | |
| level | int | YES | | NULL | |
+-------------+-------------+------+-----+---------+----------------+
用户权限表:
实现多对多关系的表
- id 主键
- user_id 用户的ID
- permission_id 权限的ID
+---------------+------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+---------------+------+------+-----+---------+----------------+
| id | int | NO | PRI | NULL | auto_increment |
| user_id | int | YES | MUL | NULL | |
| permission_id | int | YES | | NULL | |
+---------------+------+------+-----+---------+----------------+
主要解释下权限表,因为管理系统需要根据权限判断是否提供菜单,因此我将菜单作为了一级权限(parent_id=null),一级权限下的增删改查等操作定义为二级权限(parent_id=对应的一级权限)
如果用户拥有某些权限就在用户权限表中创建一条记录
后端实现
后端实现起来非常的方便,我使用的方法是定义一个注解通过AOP扫描这个注解,然后判断当前用户是否拥有对应的权限,如果没有直接抛出异常,通过全局异常处理器进行捕捉
定义注解类:
因为后台管理实现的非常简单,几乎不存在多个表之间相互嵌套,因此我用了个value表示当前Controller需要使用到的权限的ID
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface PermissionCheckAnnotation {int value();
}
定义枚举类:
主要用来定义对应权限的ID值
public class PermissionEnum {public final static int USER_QUERY = 14;public final static int USER_UPDATE = 15;public final static int USER_CREATE = 16;
}
定义AOP切面
@Pointcut("execution(扫描当前包中的全部文件) && @annotation(annotation.PermissionCheckAnnotation)")public void pt(){}@Around("pt()")public Object PermissionCheck(ProceedingJoinPoint jp) throws Throwable {MethodSignature signature = (MethodSignature) jp.getSignature();
// 反射拿到权限IDPermissionCheckAnnotation annotation = signature.getMethod().getAnnotation(PermissionCheckAnnotation.class);int value = annotation.value();// 获取用户IDInteger userId = UserContext.getUserId();// 查询当前用户和权限ID之间是否存在关联关系,如果不存在抛出异常Integer hasPermission = userPermissionMapper.hasPermission(value, userId);if (hasPermission == null || hasPermission == 0){throw new PermissionException("权限不足,请联系管理员");}return jp.proceed();}
定义controller
@PermissionCheckAnnotation(PermissionEnum.USER_CREATE)@PostMapping("/createUser")public Result<String> createUser() {}
后端修改权限
前端将修改完的权限发送过来,大体格式就是一个整数数组([1,2,3,4,5]),表示当前用户的全部权限的ID集合
权限更新的主要思路就是将该用户的全部权限删除,再重建
如果嫌弃太慢的话,我觉得可以取交集之类的操作,提升效率,或者再额外定义一个字段,用来判断当前用户权限的状态(delete字段)这样应该会比delete insert效率高一些
不过俺是个菜鸟小白+懒狗,怎么简单怎么来了
删除用户不拥有的权限
<delete id="deletePermission">delete from user_permission where user_id = #{userId}and permission_id not in<foreach item="item" collection="permissionList" open="(" close=")" separator=",">#{item}</foreach></delete>
插入新权限
因为是全部插入,权限可能会产生重复,因此我给user_id和permission_id添加唯一约束,并在插入时忽略错误
<insert id="createPermission">insert ignore into user_permission<foreach collection="permissionList" open="values" item="item" separator=",">(null,#{userId},#{item})</foreach></insert>
前端
我前端不是很好,只会单纯画个界面,因此我还没有实现按钮级别的权限控制,不过大体思路是自定义指令,然后检查是否拥有对应的权限,如果没有权限直接隐藏该按钮
至于菜单级别的权限,我的做法是每次都发送一个请求,判断能用多少菜单,即使被人扒出来其他界面,后端也做了权限的校验,数据还是拿不到
这里主要记录下如何在前端修改权限
查询返回的数据格式大致如下
{id: 15level: 2name: "操作查询"parentId: 1remark: "操作菜单下的查询功能"status: 1
}
{id: 1level: 1name: "操作菜单"parentId: nullremark: "操作菜单"status: 1
}
我想要在element-Plus中的树形控件中实现展示,操作功能,大致需要将上面的数据变成这样的格式
const data: Tree[] = [{label: 'Level one 1',children: [{label: 'Level two 1-1',},],},
]
这里通过dfs实现的(同层权限之间不会产生交集,所以应该能用最小生成树完成,不过俺是个懒狗,写了个最简单的方式)
const st = new Map();for (let i = 0; i < res.length; i++) {st.set(res[i].id, 0);
}
const list = [];
for (let i = 0; i < res.length; i++) {if (res[i].parentId == null) {const children = dfs(i, res);list.push(children);}
}
tree.value = list;function dfs(idx, res) {st.set(res[idx].id, 1);const children = []for (let i = 0; i < res.length; i++) {if (st.get(res[i].id) == 1) {continue;}if (res[i].parentId == res[idx].id) {children.push(dfs(i, res))}}if (children.length == 0)return { id: res[idx].id, label: res[idx].name }return { id: res[idx].id, label: res[idx].name, children }
}
然后把操作完的数据扔到elTree中
<el-tree v-loading="treeLoading"
@check="checkChange"
style="max-width: 200px"
ref="elTreeRef"
:data="tree"
:props="defaultProps"
show-checkbox
node-key="id" />
然后就是用户的初始权限,通过点击不同的用户向后端查询该用户拥有的权限,el-tree有个属性可以设置默认勾选的复选框,但我不知道为什么点击不同用户时,树中上一个勾选的复选框不会被取消,因此使用了el-tree的方法,当点击不同用户时触发,传入的数据是一个整数数组([1,2,3]),表示勾选的节点的id,需要在el-tree标签中提前指定node-key
elTreeRef.value.setCheckedKeys(hasPermission.value)
最后就是权限修改,大致就是用户点击不同的复选框,选择或取消不同的权限,通过绑定@check事件,然后获取里面的数据,这里说一下当点击子复选框时,父复选框也会触发一次,当父选框下的全部子复选框都取消时,父复选框也会取消,通过下面这个方法能拿到当前勾选的复选框,然后发送数据
const checkChange = function (data1, data2) {currentPermission = [...data2.checkedKeys, ...data2.halfCheckedKeys]}
总结
就这样,实现了一个简单的权限校验模型,虽然缺少了角色表,但大体功能上大差不差,添加上角色表后,可能还得考虑权限的迁移,权限的分配等等问题
不过对于一个简单的系统来说还是十分够用力
还有为什么csdn还对封面图大小设置了,把我的卡兹米都压成什么样了😭😭😭