基于SpringBoot与RBAC的功能权限设计

1. 核心架构设计

采用标准的 RBAC (Role-Based Access Control) 模型,并在此基础上融合了 SaaS 多租户(Multi-Tenancy) 的隔离机制。

1.1 实体关系模型 (ERD)

系统的权限控制由以下五张核心表和一张租户约束表构成:

  • system_user (用户表):主体,系统的登录账户。
  • system_role (角色表):权限的集合体,连接用户与菜单的桥梁。
  • system_menu (菜单/权限表) :客体,定义了页面节点(Menu)和功能操作标识(Permission,如 system:user:add)。
  • system_user_role (用户-角色关联表):实现多对多关系,决定用户拥有哪些角色。
  • system_role_menu (角色-菜单关联表):实现多对多关系,决定角色拥有哪些权限。
  • system_tenant_package (租户套餐表)前置约束。定义了该租户下所有角色能拥有的最大权限集合(全集 U 的子集)。

2. 模块一:角色菜单分配 (Role-Menu Authorization)

此模块的功能是定义"某个角色能做什么"。流程遵循 "查询全量 -> 查询已选 -> 计算差集 -> 增量更新" 的标准模式。

2.1 交互流程分析

  1. 加载全量菜单树
  • 前端调用 /list-all-simple
  • 租户过滤(关键) :后端并不直接返回 system_menu 的全量数据,而是先查询当前租户对应的 TenantPackage
  • 逻辑Result = AllMenus ∩ TenantPackageMenus。只有租户购买了的功能,才会显示在可选列表中。
  1. 加载已选菜单
  • 前端调用 /list-role-menus?roleId=X
  • 后端查询 system_role_menu 表,返回该角色已拥有的菜单 ID 集合。
  1. 提交变更
  • 前端提交 roleId 和新的 menuIds 集合。
  • 后端执行事务更新。

2.2 核心代码实现:增量更新 (Incremental Update)

为了最小化数据库锁竞争和提高性能,不采用"全删全插"策略,而是通过计算差集进行精准操作。

java 复制代码
@Transactional(rollbackFor = Exception.class)
@CacheEvict(value = RedisKeyConstants.MENU_ROLE_ID_LIST, allEntries = true) // 清空关联缓存
public void assignRoleMenu(Long roleId, Set<Long> menuIds) {
    // 1. 查询当前数据库中该角色拥有的菜单 ID
    Set<Long> dbMenuIds = convertSet(roleMenuMapper.selectListByRoleId(roleId), RoleMenuDO::getMenuId);
    
    // 2. 预处理前端传入的 ID 集合(判空处理)
    Set<Long> newMenuIds = CollUtil.emptyIfNull(menuIds);
    
    // 3. 计算需要【新增】的集 (Create Set = New - Old)
    Collection<Long> createMenuIds = CollUtil.subtract(newMenuIds, dbMenuIds);
    
    // 4. 计算需要【删除】的集 (Delete Set = Old - New)
    Collection<Long> deleteMenuIds = CollUtil.subtract(dbMenuIds, newMenuIds);
    
    // 5. 执行批量插入
    if (CollUtil.isNotEmpty(createMenuIds)) {
        // 使用 Lambda 将 ID 列表转换为实体对象列表
        List<RoleMenuDO> batchList = CollectionUtils.convertList(createMenuIds, menuId -> {
            RoleMenuDO entity = new RoleMenuDO();
            entity.setRoleId(roleId);
            entity.setMenuId(menuId);
            return entity;
        });
        roleMenuMapper.insertBatch(batchList);
    }
    
    // 6. 执行批量删除
    if (CollUtil.isNotEmpty(deleteMenuIds)) {
        roleMenuMapper.deleteListByRoleIdAndMenuIds(roleId, deleteMenuIds);
    }
}

2.3 技术要点

  • 缓存清理 :使用 @CacheEvict(allEntries = true)。由于角色菜单的变更可能影响大量用户的权限判定,且难以精确计算受影响的 Key,因此选择直接清空该缓存命名空间下的所有数据,利用 Redis 的 SCAN/DEL 机制保证数据强一致性。

3. 模块二:用户角色分配 (User-Role Assignment)

此模块的功能是定义"某个用户是谁"。其实现逻辑与角色菜单高度对称。

3.1 交互流程分析

  1. 加载全量角色列表
  • 前端调用 /role/list-all-simple
  • 数据隔离 :后端强制追加 WHERE tenant_id = current_tenant_id,防止越权查看其他租户的角色数据。
  1. 加载已拥有角色
  • 前端调用 /permission/list-user-roles?userId=Y
  • 后端查询 system_user_role 表,返回该用户的角色 ID 集合。
  1. 提交变更
  • 前端提交 userId 和新的 roleIds 集合。
  • 后端进行安全校验后执行更新。

3.2 核心代码实现与安全校验

java 复制代码
@Transactional(rollbackFor = Exception.class)
@CacheEvict(value = RedisKeyConstants.USER_ROLE_ID_LIST, key = "#userId") // 精确清除该用户的缓存
public void assignUserRole(Long userId, Set<Long> roleIds) {
    // 【安全校验】:校验操作员是否有权分配这些角色
    // 防止低级别管理员分配高级别角色(如普通管理员分配超级管理员权限)
    validateRoleLevel(roleIds); 

    // 1. 查询数据库中该用户当前的角色
    Set<Long> dbRoleIds = userRoleMapper.selectRoleListByUserId(userId);
    
    // 2. 计算差集 (Diff)
    Collection<Long> createRoleIds = CollUtil.subtract(roleIds, dbRoleIds);
    Collection<Long> deleteRoleIds = CollUtil.subtract(dbRoleIds, roleIds);
    
    // 3. 执行数据库操作 (逻辑同上)
    if (CollUtil.isNotEmpty(createRoleIds)) {
        userRoleMapper.insertBatch(userId, createRoleIds);
    }
    if (CollUtil.isNotEmpty(deleteRoleIds)) {
        userRoleMapper.deleteListByUserIdAndRoleIds(userId, deleteRoleIds);
    }
}

3.3 技术要点

  • 精确缓存清理 :与 Role-Menu 不同,这里只影响单个用户的权限。因此 @CacheEvict 使用 key = "#userId",通过 SpEL 表达式精确删除 user_role_ids::userId 这一条 Redis 记录,避免误伤其他用户缓存。
  • 权限级别管控 :在分配前必须校验 Role Level,确保 Level(操作员) >= Level(目标角色)

4. 运行时鉴权逻辑 (Runtime Verification)

在判断"用户是否有权执行某个操作"时,本系统摒弃了教科书式的"正向查找",采用了基于缓存的 "逆向倒推" 策略。这种设计源于"菜单即权限"的数据结构(即权限标识寄生在菜单表中,没有独立的权限表)。

4.1 常规方案(Standard RBAC)

大多数标准 RBAC 系统的鉴权逻辑是 "正向遍历"

  1. 加载 :根据 UserIdRoles,再根据 Roles 查出所有的 Permissions 集合。
  2. 比对 :判断目标权限字符串(如 system:user:delete)是否存在于该用户的权限集合中。
  • 缺点:在"菜单与权限合一"的架构下,若要获取用户的所有权限标识,需要关联查询整张菜单表,数据量大时效率略低。

4.2 本系统的方案:逆向白名单(Reverse Lookup Strategy)

本系统采用的是 "由权限找角色,再看用户在不在角色里" 的逻辑。

  • 核心思想 :不问"用户有什么权限",而是问 "谁有资格访问这个权限",然后看用户是否在资格名单里。
  • 实现优势:极度依赖 Redis 缓存,将复杂的数据库关联转化为内存中的 Set 交集运算。
核心代码逻辑推演 (hasAnyPermission 方法)

步骤一:由【权限标识】反查【菜单 ID】

系统首先询问:"哪个菜单携带了 system:user:delete 这个标识?"

java 复制代码
// 查缓存:Permission -> MenuId List
List<Long> menuIds = menuService.getMenuIdListByPermissionFromCache(permission);
  • 注:这里返回 List 是为了兼容极端情况下,多个不同菜单使用了同一个权限标识的情况。

步骤二:由【菜单 ID】反查【角色 ID 白名单】

系统接着询问:"ID 为 1024 的这个菜单,被授权给了哪些角色?"

java 复制代码
// 查缓存:MenuId -> RoleId Set (白名单)
Set<Long> allowedRoleIds = roleMenuService.getMenuRoleIdListByMenuIdFromCache(menuId);
  • 结果:得到一个允许访问的角色 ID 集合,例如 [1, 5](超级管理员、人事经理)。

步骤三:获取【当前用户的角色集合】

获取当前登录用户持有的角色列表。

java 复制代码
// 查缓存:UserId -> RoleId Set
Set<Long> userRoleIds = userRoleService.getRoleListByUserIdFromCache(userId);
  • 结果:例如 [5, 6](人事经理、考勤员)。

步骤四:集合交集运算 (Intersection)

判断 "用户角色集""权限白名单" 是否有交集。

java 复制代码
// 只要有任意一个角色匹配,即视为有权限
if (CollUtil.containsAny(allowedRoleIds, userRoleIds)) {
    return true; // 放行
}
return false; // 拦截
  • 案例结果:[1, 5][5, 6] 存在交集 5,鉴权通过。
相关推荐
yuuki2332332 小时前
【C++】vector底层实现全解析
c++·后端·算法
华仔啊2 小时前
如何查看 SpringBoot 当前线程数?3 种方法亲测有效
java·后端
weixin_425023002 小时前
Spring Boot 生成短链接
java·spring boot·后端
shark_chili2 小时前
浅谈CPU流水线的艺术
后端
毕设源码-钟学长2 小时前
【开题答辩全过程】以 小区物业管理APP为例,包含答辩的问题和答案
java·spring boot
while(1){yan}2 小时前
Spring,SpringBoot,SpringMVC
java·spring boot·spring
秋饼2 小时前
【spring-framework 本地下载部署,以及环境搭建】
java·后端·spring
程序员泠零澪回家种桔子2 小时前
ReAct Agent 后端架构解析
后端·spring·设计模式·架构
程序员阿鹏2 小时前
RabbitMQ持久化到磁盘中有个节点断掉了怎么办?
java·开发语言·分布式·后端·spring·缓存·rabbitmq