作者简介:大家好,我是smart哥,前中兴通讯、美团架构师,现某互联网公司CTO
联系qq:184480602,加我进群,大家一起学习,一起进步,一起对抗互联网寒冬
今天我们来聊聊如何自定义一个简单易用的权限管理系统。
虽然市面上已经有很多出色的权限管理框架,比如Spring Security或者Shiro,但很多时候我们并不需要那么多功能,特别是Spring Security,学习成本太高了!我曾经有一位同事,为了做用户权限管理,硬是引入了当时大家都不太熟悉的Spring Security,然后整了一个多星期,合并代码后直接把整个组的接口都拦截了,我直接好家伙。关键是他也不知道哪里配错了,搞了很久还是没解决,最终只好回滚代码,耽误了大半天时间。
所以,技术选型绝对是一门学问,simple is better,不要过度设计,挑最趁手的干活即可。
说到一个系统的权限管理,大家肯定不陌生,可以用一句大白话概括为:你是谁,能访问什么页面(接口)。
我们可以从中抽象出三个要素:
- 你:用户
- 是谁:角色
- 能访问什么:权限
也就是最经典的RBAC权限设计模型(Role-Based Access Control):
- t_user
- t_user_role
- t_role
- t_role_permission
- t_permission
上面的关系,对于部分同学来说可能还有点复杂,我们由易到难逐步讨论。
登录拦截
最简单的设计方案其实是登录拦截:
- 用户
- 角色:游客和系统用户(只有两个角色)
- 权限:部分接口需要登录才能访问
由于登录状态天然就能区分“游客”和“系统用户”两个角色,而这个两个角色拥有的权限是固定的:“系统用户”能访问部分受限接口,而“游客”只能访问开放接口。
此时,“用户-角色-权限”都是非常明确且不会变动的,不需要数据库存储三者之间的关系,只需要一张用户表即可,登录就能访问所有资源。
用户即角色
如果一个系统,可选的角色简单且数量固定,比如管理员、教师、学生、游客,并且未来也不会再有新的角色产生,此时也可以偷懒,直接在用户表里添加user_type字段:
`user_type` tinyint(1) unsigned NOT NULL COMMENT '用户类型 1-管理员 2-教师 3-学生 4-游客'
如果从RBAC的角度看,其实 用户表 = t_user + t_user_role + t_role,而且一个用户只能有一个角色,用户即角色。
此时,t_role_permission和t_permission还要不要呢?
答案是:可要可不要,我个人倾向于不要。
那么,没有表结构怎么表示对象关系呢?可以直接在代码中耦合。
比如,教师能访问新增课程的接口,那么我直接在代码里写死:
@PostMapping("/saveCourse")
public Result<Boolean> saveCourse(@RequestBody Course course) {
// 权限校验
User currentUser = (User) session.getAttribute(WebConstant.CURRENT_USER_IN_SESSION);
if (!UserTypeEnum.TEACHER.getType().equals(currentUser.getUserType()) {
throw new BizException(ExceptionCodeEnum.PERMISSION_ERROR);
}
return Result.success(courseService.saveCourse(course));
}
或者逼格高一点,使用注解:
@PermissionRequired(UserTypeEnum.TEACHER)
@PostMapping("/saveCourse")
public Result<Boolean> saveCourse(@RequestBody Course course) {
return Result.success(courseService.saveCourse(course));
}
在“够用”的前提下,这种方案最大的好处就是:直观且编码简单(和登录拦截相比只是用户表多了一个user_type,拦截逻辑改改即可)。
之所以能这么设计,还是因为需求允许。
我们来看一下RBAC五张表在当前模式中是什么情况。
用户表t_user肯定不能删,保留着。但由于用户只能有一种角色:
- 把t_role表的数据移到代码中,用UserTypeEnum表示(反正角色就几个,就没必要存表里),所以t_role可以删除
- 在t_user中新增user_type字段,直接绑死t_user和t_role的对应关系,所以t_user_role也不需要了
用编码的方式手动指定“角色-权限”关系,消除了t_role_permission和t_permission:
- @PermissionRequired(UserTypeEnum.ADMIN)注解的接口:只能管理员访问
- @PermissionRequired(UserTypeEnum.TEACHER)注解的接口:只能教师访问
- ...
所以此时RBAC只剩一张t_user表。
至于有些接口多种角色都能访问怎么办呢?
@PermissionRequired(value = {UserTypeEnum.ADMIN, UserTypeEnum.TEACHER}, logical = Logical.OR)
@PostMapping("/saveCourse")
public Result<Boolean> saveCourse(@RequestBody Course course) {
return Result.success(courseService.saveCourse(course));
}
相信大家都能看懂。
RBAC
但是很多时候,我们还是期望将系统做成可扩展的,此时就需要借助RBAC来搞一下了。
很多人对于RBAC其实挺陌生的,特别是非科班转行的同学,听到“RBAC”脑子全是糊的,可能也从来没搞懂过,只能支支吾吾说几句:我记得好像有什么用户表、角色表、权限表啥的...
我个人把实现RBAC的过程分为两部分:
- RBAC五张表的增删改查(简单)
- 基于五张表做权限控制(不简单)
表的增删改查
什么意思呢?相对设计促销活动、拉新活动什么的,RBAC的表设计及增删改查几乎是没有任何难度的:
t_user
id | name | password | create_time | update_time | deleted |
1 | bravo1988 | 123456 | 2021-02-14 15:28:12 | 2021-02-14 15:28:14 | 0 |
2 | 毛晓彤 | 654321 | 2021-02-14 15:28:32 | 2021-02-14 15:28:34 | 0 |
t_user_role
id | user_id | role_id |
1 | 1 | 1 |
2 | 1 | 2 |
3 | 2 | 1 |
t_role
id | name | info | create_time | update_time | deleted |
1 | ADMIN | 管理员 | 2021-02-14 15:28:12 | 2021-02-14 15:28:14 | 0 |
2 | TEACHER | 教师 | 2021-02-14 15:28:32 | 2021-02-14 15:28:34 | 0 |
3 | STUDENT | 学生 | 2021-02-14 15:28:32 | 2021-02-14 15:28:34 | 0 |
t_role_permission
id | role_id | permission_id |
1 | 1 | 1 |
2 | 1 | 2 |
3 | 2 | 3 |
4 | 2 | 4 |
5 | 2 | 5 |
t_permission
id | module | name | create_time | update_time | deleted |
1 | 用户 | 新增用户 | 2021-02-14 15:28:12 | 2021-02-14 15:28:14 | 0 |
2 | 用户 | 拉黑用户 | 2021-02-14 15:28:32 | 2021-02-14 15:28:34 | 0 |
3 | 课程 | 新增课程 | 2021-02-14 15:28:32 | 2021-02-14 15:28:34 | 0 |
4 | 课程 | 修改课程 | 2021-02-14 15:28:32 | 2021-02-14 15:28:34 | 0 |
5 | 课程 | 删除课程 | 2021-02-14 15:28:32 | 2021-02-14 15:28:34 | 0 |
这些表数据,都要提供对应的CRUD接口,但都很简单。t_user、t_role、t_permission就是单表CRUD就不说了,它们之间的关系维护也不难,以t_user_role为例:
@PermissionRequired(UserTypeEnum.ADMIN)
@PostMapping("/assignRoles")
public Result<Boolean> assignRoles(@RequestBody UserRoleDTO userRoleDTO) {
List<Long> roleIds = userRoleDTO.getRoleIds();
roleIds.forEach(roleId->{
UserRole userRole = new UserRole();
userRole.setUserId(user.getUserId());
userRole.setRoleId(roleId);
userRoleMapper.insert(userRole);
});
return Result.success(Boolean.TRUE);
}
@Data
static class UserRoleDTO {
private Long userId;
private List<Long> roleIds;
}
动态权限控制
RBAC真正的难点是如何基于这五张表动态地进行权限控制,核心是“动态”。比如,上面讲的在t_user表中增加user_type并直接使用注解的方案,其实属于硬编码。它的缺点是,如果运营告诉你,由于图书信息录入错误率较高,希望让学生也参与校正(对学生开放updateBook接口),你只能改代码,把接口上面的:
@PermissionRequired(value={UserTypeEnum.TEACHER})
改为:
@PermissionRequired(value={UserTypeEnum.TEACHER, UserTypeEnum.STUDENT}, logical = Logical.OR)
然后重新打包部署,如果你们公司没有做持续集成啥的,可能这个需求还要等到夜深人静没什么用户时才能停机重启...
所以RBAC最难的不是表的维护,而是权限控制,且重点是支持“动态权限分配”!
所谓“动态权限分配”是怎么回事呢?其实只要能解决本小节开头那个问题就算实现动态权限分配了。
一起来考虑实现思路吧~
让我们重新把关注点放在刚才的问题及解决办法上:我们通过修改@PermissionRequired的属性,让学生这个角色也能访问updateBook接口。
那么,不修改这个注解的属性可以吗?有没有其他办法呢?
不知道大家有没有意识到,现在@PermissionRequired这个注解代表的其实是角色(因为属性是UserType),而接口是资源,也就是权限,把代表角色的@PermissionRequired直接放在代表权限的接口上,从某种意义上来说,已经耦合了(当前资源只能被这个角色访问),所以后期要让这个资源被其他角色访问,就必须修改@PermissionRequired注解,也就必须修改代码。
有个“小聪明”可以解决这个问题:“用户-角色-权限”是两两关联的,“角色-权限”虽然被绑死了,但可以通过更改“用户-角色”来弥补。
把原本是学生的小李改为user_type=2,那么小李就从学生变为教师,接口没改,但是数据库表的关联关系已经变了,下次小李请求这个接口时,查询t_user_role得知小李是教师角色,所以小李也能改图书信息。
但这样做合适吗?
正确的处理办法是,@PermissionRequired不应该代表角色,而应该代表权限本身,如此一来代表资源的接口和代表权限的@PermissionRequired可以看做同一类事物,不存在硬编码绑死的问题(其实还可以优化,后面会提到)。至于如何动态更改接口访问权限,还是老办法,通过往中间表插入数据,让学生这个角色也拥有“编辑图书”的权限即可。
t_role
id | name | info | create_time | update_time | deleted |
3 | STUDENT | 学生 | 2021-02-14 15:28:32 | 2021-02-14 15:28:34 | 0 |
t_role_permission(其他两张表都不用动,只修改中间表,分配权限即可)
id | role_id | permission_id |
1 | 3 | 1 |
2 | 3 | 2 |
t_permission
id | module | name | create_time | update_time | deleted |
1 | 课程 | 提交作业 | 2021-02-14 15:28:12 | 2021-02-14 15:28:14 | 0 |
2 | 课程 | 修改课程信息 | 2021-02-14 15:28:32 | 2021-02-14 15:28:34 | 0 |
正所谓磨刀不误砍柴工,希望这篇文章能帮大家理清一些基本的概念及思路,为后面的权限管理实战打好基础。部分同学对于RBAC望而却步,可能是因为之前看到过RBAC还涉及到树形结构等,看起来很复杂,有畏难情绪。其实没必要关心前端的展现形式,只要站在后端的角度提供好相应的接口接口。况且我们已经学过树形结构,前端要什么给什么,不带怕的。
后面几篇我们就按照:登录拦截、用户及角色、RBAC的顺序学习。
作者简介:大家好,我是smart哥,前中兴通讯、美团架构师,现某互联网公司CTO
进群,大家一起学习,一起进步,一起对抗互联网寒冬