基于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,鉴权通过。
相关推荐
智驭未来掌门人27 分钟前
利用FFmpeg快速实现一个RTSP推流服务器
后端
垚垚学技术_聚焦云原生40 分钟前
K8s Pod 完整生命周期详解
后端
JavaGuide44 分钟前
Github 史诗级故障,与此同时,Cursor 版「GitHub」正式上线!
前端·后端
不一样的少年_1 小时前
修了 Bug、做了重构,为什么老板还是觉得你没产出?
前端·后端·程序员
水深火乐1 小时前
安全地生成验证码、密码和 Token
后端
小强19881 小时前
SQL Server 慢查询怎么定位?一套从“卡”到“快”的完整排查流程
后端
水深火乐1 小时前
interface和any
后端
步行cgn1 小时前
MyBatis Error evaluating expression ‘ids‘. Return value (3) was not iterable 错误详
java·后端
神奇小汤圆1 小时前
Spring Boot + LangChain4j实现RAG——从零搭建企业级知识库问答系统
后端
H_Peak1 小时前
Python运算符与流程控制:if条件判断
后端