RBAC权限模型
最近在写一个项目,原本在权限控制模块使用的是层级的权限控制模型。而项目涉及到交错、交叉的权限,所以层级的权限控制就显得非常弱小。故而去学习了RBAC的权限模型,写了一篇学习笔记。
什么是RBAC权限模型?
RBAC(Role-Based Acess Control)即基于角色的访问控制。
其内涵可以用简单的一句话进行概括。即—— 权限分配给角色,角色分配给用户,以此完成权限的控制。这也是RBAC权限模型中已定义的概念,角色、权限、分配、用户。
- 角色(Role): 是对一组权限集合的抽象。
- 权限(Permission): 是对系统指定资源或操作的许可或通行证。
- **用户(User):**系统的主要使用者,一个用户可以被分配一个或多个角色。
- 分配(Assignment): 将用户与角色联系,让用户拥有具体的权限。
为什么需要RBAC权限模型?
非角色的权限模型缺点
- 在传统的权限模型中。如果简简单单,仅用一个Role字段,来控制用户对资源或者操作的访问和使用,过于粗糙,不易于控制、且极容易出错,风险过高。
- 但若采用扁平限模型(直接分配权限模型)时,如果有新的用户,因为有大部分用户都是具有相似甚至相同的权限的,所以需要对其进行重复的、繁琐的权限分配这一操作。这个过程中不仅繁琐、浪费时间,还容易不小心导致出错。
- 面对交叉权限需求时,直接分配权限模型极易容易导致混乱。如下面的**例子,**这种类似情况通常在后台管理系统,对管理员权限进行精细控制时尤为严重。
例子:
一个二手市场同时混合了电商元素的系统,其可能会有"要求商家用户在拥有普通用户的权限(如发布二手商品等)的同时,拥有商家特有的店铺权限,即该账户同时拥有了'商家'和'用户'两个权限'"。这种情况下,如果商家的业务发生变化,如新增了一个功能。这时就需要在大量账户中匹配到商家账户并为其添加该权限,这极大增加了维护成本。
通俗来说: 在直接分配权限模型中,对于一些高精度权限控制需求的实现,会极大增加现有权限模块的复杂程度,降低效率和性能,导致日常维护困难,且权限模块的复杂化实现也会大大增加维护成本。
RBAC权限模型优点
- 将权限集合抽离出来,按使用者分类存放在角色这一集合体中。仅为用户赋予角色,不直接赋予权限。实现了权限与用户的解耦。
- 当有权限变动时,只需更改角色拥有的权限。而不需要最每一个用户单独重复手动处理。效率高。
- 当有更交错复杂的权限控制时,RBAC模型可以在角色的基础上附加其他条件,以此实现更精细的控制,更简便、效率高。
RBAC权限模型的主要分类
还有一些更有针对性的RBAC权限模型变体,他们针对特定领域有更好的适配性和可靠性。这里不再举例,仅展示了RBAC的常见以及主要模型
| 模型 | 特点 | 描述 |
|---|---|---|
| RBAC0 | 用户->角色->权限 | RBAC最基础的模型。其他RBAC模型均为以该模型为基础。 |
| RBAC1 | RBAC0 + 角色继承 | 在RBAC0基础上增加了角色继承。即高等级角色可以继承低等级角色都所有权限 |
| RBAC2 | RBAC0 + 约束模型 | 在RBAC0基础上增加了约束模型。如互斥约束:同一用户只能有一个角色;先决约束:一个用户必须拥有某一角色后才能拥有另一个角色等等约束条件。 |
| RBAC3 | RBAC1 + RBAC2 | 集成了角色继承和约束模型。也是完整程度最高的RBAC模型,但是实现成本很高。 |
| 混合模型 | RBAC + 其他权限模型 | 这个模型并不算RBAC权限模型的范畴。其是将RBAC的优点结合其他权限模型混合使用,已达到更好的效果 |
RBAC的简单实践
这里以RBAC0作为讲解,以基于mysql等关系数据库为例实现RBAC0。
RBAC0的三个核心组件,是用户、角色、权限。
表设计
| 表 | 表字段 |
|---|---|
| 用户表 | id 其他 |
| 角色表 | id type(角色名称) 其他 |
| 用户角色关联表 | user_id role_id status 其他 |
| 角色权限关联表 | role_id permission_id status(是否启用) 其他 |
| 权限表 | id code(权限码) name(权限名称) description(权限具体描述) 其他 |
他们之间的关系为:
用户-用户角色关联表(一对多); 角色-角色权限表(一对多);
用户角色管理表-角色(多对一); 角色权限表-权限(多对一);
简单填充表数据
# 用户表
INSERT user(id,...其他)
VALUES
(1,...其他),
(2,...其他);
# 角色表
INSERT role(id,type,...其他)
VALUES
(1,'user',...其他),
(2,'merchart',...其他);
# 权限表
INSERT permission(id,code,name,description,...其他)
VALUES
(1,'user:changePassword','修改密码','更改账户密码',...其他),
(2,'merchart:publishProduct','发布店铺商品','用于商家账户发布正品全新商品',...其他);
# 用户角色关联表 (status = 1表示启用)
INSERT user_role(user_id,role_id,status,...其他)
VALUES
(1,1,1,...其他),
(1,2,1,....其他);
# 角色权限关联表 (status = 1表示启用)
INSERT role_permission(role_id,permission_id,status,...其他)
VALUES
(1,1,1,...其他),
(2,2,1,...其他),
(2,1,1,...其他);
编码权限控制逻辑
这里以Java为例,使用spring框架实现,其他语言都是相似逻辑。
定义权限注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface PermissionCheck {
/**
* 所需的角色
*/
Role Role();
/**
* 所需的权限
*/
String permission();
/**
* 权限逻辑关系
* AND: role和permission都需满足
* OR: 满足role和permission中的任意一个
* 默认: OR
*/
Logic logic() default Logic.OR;
enum Logic{
AND,
OR,
}
}
定义权限对象
@Data
@AllArgsConstructor
@NoArgsConstructor
public class permissionInfo{
private List<Role> role;
private List<String> permissionCode;
}
@AllArgsConstructor
@Getter
public enum Role {
user(1),
Merchant(2);
private final int Id;
public static Role getRoleById(int id) {
for (Role role : Role.values()) {
if (role.getId() == id) {
return role;
}
}
throw new IllegalArgumentException("错误的角色码, Id:"+ id);
}
}
定义sql服务类
// 这里简单只展示sql,使用mybatis框架 其他自行补充
@Mapper
public interface permissionMapper{
@Select("""
SELECT
rp.role_id AS role
p.code AS permission_code,
FROM role r
JOIN role_permission rp ON r.id = rp.role_id
JOIN permission p ON p.id = rp.permission_id
WHERE r.user_id = #{userId}
""")
// 这里涉及到 与对象映射时的多个记录 一个对象接收问题 因篇幅问题 这里简单写的 具体需要具体解决
permissionInfo getPermissions(Long userId)
}
定义切面类
@Aspect
@Component
@RequiredArgsConstructor
@Slf4j
public class PermissionAspect {
private final PermissionCacheService permissionCacheService;
@Around("@annotation(permissionCheck)")
public Object checkPermission(ProceedingJoinPoint joinPoint, PermissionCheck permissionCheck) throws Throwable {
//效验权限
boolean result1 = checkPermission(xxxx,xxxx,xxxx);
//效验角色
boolean result2 = checkRole(xxxx,xxxx,xxxx)
if(permissionCheck.logic().equals(PermissionCheck.Logic.AND){
return result1 & result2 ? joinPoint.proceed() : false; //或者抛出异常、记录日志等操作
}
return result1 == true ? joinPoint.proceed() : false; //或者抛出异常、记录日志等操作
}
}
使用注解
@PermissionCheck(Role = Role.user,permission = "user:changePassword",logic = PermissionCheck.Logic.OR)
// 指定角色 和 权限 logic默认为OR 这里显然不适合使用AND 所以直接使用默认logic即可
public Result<String> updatePassword(
@Parameter(description = "密码修改信息", required = true)
@RequestBody @Valid UserUpdatePwDTO updatePw,
@Parameter(description = "用户编号", required = true)
@PathVariable String userSn,
@RequestHeader("Authorization")
String token
){
userService.updatePassword(updatePw, userSn,token);
return Result.success();
}
到此已经成功实现了一个基础的RBAC0的权限控制模块。
当然这是使用mysql关系型数据库实现的,在进入接口处频繁的查库会造成性能浪费和过度消耗。所以在此基础上可以增设Redis等缓存层,来缓存权限信息,以此达到更好的性能,降低数据库压力。
当然你可以基于项目的难度,采用更完整的RBAC权限模型。亦或者采用混合模型,取RBAC权限模型的优点,与其他权限模型的优点进行混合使用,以达到更高效更可靠的权限系统。
比如二手市场权限控制时,可以采用RBAC + 权限禁用 。 即在RBAC0基础上 ,再添加一个表,用户标识单个用户被禁用的权限。这样在用户访问接口时,可以先效验是否被禁用,以此更好地适配电商等场景对一些异常风险账户操作的限制。