若依 RBAC 权限控制学习笔记

若依 RBAC 权限控制学习笔记

整理自关于若依权限体系的完整讨论,涵盖核心模型、认证授权流程、接口权限机制、角色授权方式,以及从硬编码 AOP 迁移到若依动态权限的完整代码示例。


一、总体认识

若依的 RBAC 权限控制不是单一技术,而是一套贯穿前后端、从接口到数据行的多层纵深防御体系。核心是在经典 "用户 → 角色 → 权限" 模型基础上,结合 Spring Security 与 JWT 进行工程化扩展。

权限划分为四个层级:

  1. 菜单权限:用户能看到哪些页面/路由。
  2. 按钮权限:页面内哪些按钮/元素显示。
  3. 接口权限:后端接口是否允许调用。
  4. 数据权限:能查看哪些数据行。

二、核心数据表设计

表名 作用
sys_user 用户表
sys_role 角色表,role_key 用于角色校验
sys_menu 菜单表,同时存储页面路由、按钮权限、接口权限标识
sys_user_role 用户与角色关联表
sys_role_menu 角色与菜单/按钮/接口权限关联表

关键字段:

  • sys_menu.menu_type:
    • M:目录
    • C:页面菜单
    • F:按钮/接口权限
  • sys_menu.perms:权限标识字符串,例如 system:user:add。
  • 管理员角色通常拥有通配权限 *:*:*。

三、认证流程:确认"你是谁"

若依采用 Spring Security + JWT + Redis 组合方案。

1. 登录认证

用户登录时,SysLoginService 调用 AuthenticationManager 触发 Spring Security 认证流程。UserDetailsServiceImpl 从数据库加载用户信息、密码并校验。

2. 权限加载

认证成功后,SysPermissionService 根据用户 → 角色 → 菜单权限,加载出该用户的权限集合 Set<String> permissions,放入 LoginUser。

3. 令牌生成与缓存

系统生成 UUID 作为 Key,将 LoginUser 存入 Redis,并生成 JWT 返回前端。后续请求通过 JWT 识别用户,同时利用 Redis 实现令牌续期与主动失效。

4. 请求拦截

每个请求到达后端时,JwtAuthenticationTokenFilter 拦截请求,解析 JWT,从 Redis 取出对应的 LoginUser 存入 SecurityContext,为后续鉴权准备数据。


四、授权机制:判断"你能做什么"

1. 接口层:方法级权限校验

若依使用 Spring Security 的 @PreAuthorize 注解和自定义 PermissionService(Bean 名称为 ss)实现接口权限控制。

示例:

java 复制代码
@PreAuthorize("@ss.hasPermi('system:user:list')")
@GetMapping("/list")
public TableDataInfo list(SysUser user) {
    return userService.selectUserList(user);
}

校验逻辑:

  • @PreAuthorize 是 Spring Security 的方法级授权注解。
  • @ss 是若依自定义的 PermissionService Bean。
  • hasPermi('system:user:list') 从当前登录用户 LoginUser 的 permissions 集合中判断是否包含该权限字符串。
  • 管理员拥有 *:*:*,直接放行。
  • 角色校验可用 @ss.hasRole('admin')。

2. 前端层:路由与按钮控制

页面/路由级:动态路由

用户登录后,前端调用 getRouters 接口获取有权访问的菜单树,通过 GenerateRoutes 动态生成 Vue Router 路由配置,避免用户直接输入 URL 访问未授权页面。

按钮/元素级:自定义指令

若依提供 v-hasPermi 和 v-hasRole 指令:

html 复制代码
<el-button v-hasPermi="['system:user:add']" @click="handleAdd">新增用户</el-button>

无权限时直接从 DOM 中移除元素。

3. 数据层:行级数据过滤

数据权限控制用户能看到哪些数据行,核心机制是 基于 AOP 的注解 + MyBatis 拦截器/动态 SQL。

若依定义 5 种数据权限范围,与角色绑定:

  1. 全部数据权限
  2. 自定数据权限
  3. 本部门数据权限
  4. 本部门及以下数据权限
  5. 仅本人数据权限

在查询方法或 Mapper 接口上添加 @DataScope 注解后,框架会根据当前用户角色配置的数据权限范围,在 SQL 后追加类似 AND dept_id IN (...) 或 AND create_by = ... 的过滤条件。


五、重点澄清:接口权限不是"AOP 动态查表放行"

常见误解:

若依接口权限是使用 AOP 切面外加动态表获取用户角色信息,给相应角色开放后端接口。

更准确的理解:

  1. 不是若依自己写 AOP 切面动态决定放行哪个接口

    接口权限校验使用的是 Spring Security 的方法级授权 @PreAuthorize。底层会经过 Spring AOP 代理/方法拦截器,但这是 Spring Security 的机制,不是若依额外写切面动态开放接口。

  2. 不是每次请求都动态查表获取角色权限

    用户角色和权限信息主要在登录认证阶段加载,放入 LoginUser 并缓存到 Redis。接口调用时只从当前登录用户上下文取权限集合做判断,不是每个接口都查 sys_user_role、sys_role_menu、sys_menu。

准确概括:

若依接口权限 = Spring Security 的 @PreAuthorize 方法级授权 + 自定义 PermissionService 校验当前登录用户权限集合。角色和权限数据在登录时加载并缓存,接口调用时只做内存/上下文判断。

角色或权限变更后,通常需要用户重新登录或刷新缓存,权限才会生效。


六、如何给角色分配后端接口权限

若依不是单独维护"接口列表"再勾选,而是通过 "菜单权限"树间接完成。核心逻辑:

后端接口的权限标识 perms 被绑定在 menu_type='F' 的菜单项上,给角色勾选这些菜单项,就等于授予该角色调用对应接口的权限。

1. 管理后台操作

入口:系统管理 → 角色管理。

  1. 编辑目标角色,点击"菜单权限"。
  2. 在树形菜单中勾选需要的权限节点,包括页面菜单及其下面的"按钮"类型节点。
  3. 保存后,系统更新 sys_role_menu 关联表。

注意:只能勾选已存在于菜单树中的权限项。新增接口权限前,通常需要先在 系统管理 → 菜单管理 中创建对应权限节点。

2. 数据链路

第一步:接口权限标识存储在 sys_menu

为接口创建一条特殊记录:

  • menu_type = 'F'
  • perms = 'system:user:add' 等权限字符串
第二步:角色与权限绑定存储在 sys_role_menu

给角色勾选权限后,sys_role_menu 插入记录,表示"某角色拥有某菜单/按钮/接口权限"。

第三步:登录时聚合权限

SysPermissionService 根据 sys_user_role 找到用户角色,再通过 sys_role_menu 找到角色关联的 menu_id,从 sys_menu 筛选 menu_type='F' 且 perms 不为空的记录,提取 perms,汇总成 Set<String> permissions,存入 LoginUser 并缓存 Redis。

第四步:接口调用时校验

@ss.hasPermi('system:user:add') 判断缓存好的权限集合中是否包含该字符串。

3. 完整链路总结

text 复制代码
sys_menu 中创建 menu_type='F' 的接口权限记录并写 perms
        ↓
角色管理界面勾选该记录,数据写入 sys_role_menu
        ↓
用户登录时聚合其角色关联的所有 perms,存入 LoginUser.permissions
        ↓
接口调用时 @PreAuthorize("@ss.hasPermi('...')") 校验集合是否包含权限字符串

七、从硬编码 AOP 迁移到若依动态权限:工期与步骤

1. 工期估算

从硬编码 @AOP 角色判断迁移到若依动态权限,核心工作量不在编码复杂度,而在存量接口盘点 + 权限数据配置的准确性。

  • 中等规模单体项目,熟练开发者通常 1~2 周 完成主体迁移和测试。
  • 系统简单:3~5 天。
  • 接口数量庞大或涉及数据权限:2~3 周甚至更久。

2. 改造步骤

阶段一:数据库与权限数据初始化(约 1~2 天)
  • 盘点所有需要权限控制的接口。
  • 为每个接口定义唯一权限标识,命名规范通常为 {模块}:{业务}:{操作},如 system:user:add。
  • 将权限标识作为 menu_type='F' 的记录插入 sys_menu。
  • 在角色管理界面为角色勾选权限节点,数据写入 sys_role_menu。
阶段二:后端代码改造(约 2~3 天)
  • 将硬编码 AOP 注解替换为 @PreAuthorize("@ss.hasPermi('模块:业务:操作')")。
  • 保证 @PreAuthorize 中的权限字符串与 sys_menu.perms 完全一致。
阶段三:前端权限点适配(约 1~2 天,如需要)
  • 将 v-if="hasRole('Admin')" 等判断替换为 v-hasPermi="['模块:业务:操作']"。
  • 权限字符串必须与后端、数据库一致。
阶段四:迁移验证与测试(约 2~3 天)
  • 使用不同角色账号登录,验证接口放行与拦截。
  • 注意 Redis 缓存:修改角色权限后,通常需要清缓存或让用户重新登录。

3. 影响工期的关键变量

  • 存量接口数量。
  • 是否涉及数据权限迁移。
  • 团队对若依 PermissionService、菜单配置流程的熟悉程度。

八、代码讲解:改造前 vs 改造后

1. 改造前:硬编码 AOP 方式

自定义注解
java 复制代码
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RequireRole {
    String[] value();
}
AOP 切面
java 复制代码
@Aspect
@Component
public class RoleAspect {

    @Around("@annotation(requireRole)")
    public Object check(ProceedingJoinPoint pjp, RequireRole requireRole) throws Throwable {
        String currentRole = UserContext.getCurrentRole();
        boolean allowed = Arrays.asList(requireRole.value()).contains(currentRole);
        if (!allowed) {
            throw new AccessDeniedException("无权限");
        }
        return pjp.proceed();
    }
}
使用方式
java 复制代码
@RequireRole({"User", "Admin"})
@GetMapping("/list")
public TableDataInfo list(SysUser user) {
    return userService.selectUserList(user);
}

问题:角色写死在注解中,新增角色或调整权限需要改代码重新发布;只能控制到角色粒度,无法控制到具体接口。


2. 改造后:若依动态权限方式

后端代码只改注解
java 复制代码
// 原来
// @RequireRole({"User", "Admin"})

// 改为
@PreAuthorize("@ss.hasPermi('system:user:list')")
@GetMapping("/list")
public TableDataInfo list(SysUser user) {
    return userService.selectUserList(user);
}
@ss 对应的 PermissionService
java 复制代码
@Service("ss")
public class PermissionService {

    private static final String ALL_PERMISSION = "*:*:*";

    public boolean hasPermi(String permission) {
        if (StringUtils.isEmpty(permission)) {
            return false;
        }
        LoginUser loginUser = SecurityUtils.getLoginUser();
        if (StringUtils.isNull(loginUser)
                || CollectionUtils.isEmpty(loginUser.getPermissions())) {
            return false;
        }
        return hasPermissions(loginUser.getPermissions(), permission);
    }

    private boolean hasPermissions(Set<String> permissions, String permission) {
        return permissions.contains(ALL_PERMISSION)
                || permissions.contains(StringUtils.trim(permission));
    }

    public boolean hasRole(String role) {
        LoginUser loginUser = SecurityUtils.getLoginUser();
        return loginUser != null && loginUser.getRoles().contains(role);
    }
}

关键点:校验时不查数据库,只判断 LoginUser.permissions 集合是否包含权限字符串。

登录时权限加载:SysPermissionService
java 复制代码
@Component
public class SysPermissionService {

    @Autowired
    private ISysMenuService menuService;

    public Set<String> getRolePermission(SysUser user) {
        Set<String> roles = new HashSet<>();
        if (user.isAdmin()) {
            roles.add("admin");
        } else {
            roles.addAll(menuService.selectMenuPermsByUserId(user.getUserId()));
        }
        return roles;
    }

    public Set<String> getMenuPermission(SysUser user) {
        Set<String> perms = new HashSet<>();
        if (user.isAdmin()) {
            perms.add("*:*:*");
        } else {
            List<SysRole> roles = user.getRoles();
            if (!CollectionUtils.isEmpty(roles)) {
                for (SysRole role : roles) {
                    Set<String> rolePerms = menuService.selectMenuPermsByRoleId(role.getRoleId());
                    role.setPermissions(rolePerms);
                    perms.addAll(rolePerms);
                }
            } else {
                perms.addAll(menuService.selectMenuPermsByUserId(user.getUserId()));
            }
        }
        return perms;
    }
}
UserDetailsServiceImpl 组装 LoginUser
java 复制代码
@Override
public UserDetails loadUserByUsername(String username) {
    SysUser user = userService.selectUserByUserName(username);
    Set<String> userPermissions = permissionService.getMenuPermission(user);
    return new LoginUser(user.getUserId(), user.getDeptId(), user, userPermissions);
}
权限查询 SQL 示例
xml 复制代码
<select id="selectMenuPermsByUserId" resultType="String">
    select distinct m.perms
    from sys_menu m
    left join sys_role_menu rm on m.menu_id = rm.menu_id
    left join sys_user_role ur on rm.role_id = ur.role_id
    where m.status = '0'
      and m.perms is not null and m.perms != ''
      and ur.user_id = #{userId}
</select>
数据配置:新增接口权限并授权
sql 复制代码
-- 1. 在菜单表新增接口/按钮权限节点
INSERT INTO sys_menu (
    menu_name, parent_id, order_num, path, component,
    is_frame, is_cache, menu_type, visible, status,
    perms, icon, create_by, create_time, remark
) VALUES (
    '用户查询', 100, 1, '', NULL,
    1, 0, 'F', '0', '0',
    'system:user:list', '#', 'admin', sysdate(), ''
);
-- 假设生成 menu_id = 1001

-- 2. 角色与权限节点绑定
INSERT INTO sys_role_menu (role_id, menu_id) VALUES (2, 1001);
INSERT INTO sys_role_menu (role_id, menu_id) VALUES (3, 1001);
前端同步改造
html 复制代码
<!-- 改造前 -->
<el-button v-if="currentRole === 'Admin'">新增用户</el-button>

<!-- 改造后 -->
<el-button v-hasPermi="['system:user:add']" @click="handleAdd">新增用户</el-button>

v-hasPermi 指令核心逻辑:

js 复制代码
export default {
  mounted(el, binding) {
    const { value } = binding;
    const all_permission = "*:*:*";
    const permissions = store.getters && store.getters.permissions;

    if (value && value instanceof Array && value.length > 0) {
      const permissionFlag = value;
      const hasPermissions = permissions.some(permission => {
        return all_permission === permission || permissionFlag.includes(permission);
      });
      if (!hasPermissions) {
        el.parentNode && el.parentNode.removeChild(el);
      }
    } else {
      throw new Error(`请设置操作权限标签值`);
    }
  }
};

九、完整迁移示例:删除用户接口

① 后端代码

java 复制代码
@PreAuthorize("@ss.hasPermi('system:user:remove')")
@DeleteMapping("/{userIds}")
public AjaxResult remove(@PathVariable Long[] userIds) {
    return toAjax(userService.deleteUserByIds(userIds));
}

② 数据库插入权限节点

sql 复制代码
INSERT INTO sys_menu (
    menu_name, parent_id, order_num, menu_type, perms, status
) VALUES (
    '用户删除', 100, 2, 'F', 'system:user:remove', '0'
);

③ 角色授权

sql 复制代码
INSERT INTO sys_role_menu (role_id, menu_id)
VALUES (2, <上一步生成的 menu_id>);

④ 前端按钮

html 复制代码
<el-button
  v-hasPermi="['system:user:remove']"
  type="danger"
  @click="handleDelete">
  删除
</el-button>

⑤ 验证

  • 管理员登录:看到按钮并能删除。
  • 普通角色未勾选:按钮不显示,直接调接口返回 403。

十、迁移对照总表

维度 改造前:硬编码 AOP 改造后:若依动态权限
接口注解 @RequireRole({"User","Admin"}) @PreAuthorize("@ss.hasPermi('system:user:list')")
权限数据存放 代码中写死 sys_menu.perms
角色授权 改代码重新发布 后台勾选,写 sys_role_menu
校验时机 每次请求走 AOP 登录时聚合成 Set,请求时 contains
前端控制 v-if="role==='Admin'" v-hasPermi="['system:user:add']"
权限粒度 角色级 接口/按钮级
动态调整 需改代码 改数据并刷新缓存/重新登录

十一、易错点与总结

易错点

  1. 三端权限字符串必须完全一致

    sys_menu.perms = @ss.hasPermi('...') = v-hasPermi="['...']"。大小写、分隔符、拼写错一个,就会导致前端不显示或接口 403。

  2. 改完权限要清缓存或重新登录

    权限在登录时加载进 Redis。后台修改角色权限后,旧 token 中的 LoginUser.permissions 仍是旧数据,必须让用户重新登录或刷新缓存。

  3. 不要误以为每次接口请求都查权限表

    接口调用时只判断 LoginUser.permissions 集合,真正的查表发生在登录阶段。

总结

若依 RBAC 的完整体系:

  • 认证 :Spring Security + JWT + Redis,登录时加载用户和权限,缓存 LoginUser。
  • 接口授权 :@PreAuthorize("@ss.hasPermi('...')") 校验当前用户权限集合。
  • 前端授权 :动态路由 + v-hasPermi / v-hasRole 指令。
  • 数据授权 :@DataScope 注解 + AOP + 动态 SQL 实现行级过滤。
  • 角色授权接口权限 :通过 sys_menu 的 menu_type='F' 记录承载 perms,角色勾选后写入 sys_role_menu,登录时聚合为权限集合。

从硬编码 AOP 迁移到若依动态权限,代码层面每个接口通常只改一行注解,主要工作量和风险在存量接口盘点、权限字符串配置一致性、角色授权数据以及缓存刷新验证上。

相关推荐
景熙55231 小时前
2.什么是业务后台系统
java·spring·tomcat·mybatis·jdbc
mewowaBc1 小时前
使用Pinia持久化存储实现用户认证系统
vue·pinia·用户认证系统
电梯界知识分子1 小时前
景德镇老房重新设计阶段规划楼梯与电梯,1.7m×1.6m空间装400kg观光电梯,棕色框架茶色玻璃适配中式装修
大数据·服务器·前端·网络·家用电梯
古法老登瞎折腾1 小时前
大数据智能运维实战(八):从调度任务追到计算引擎——补齐排查链路
java·大数据·运维·spring ai·mcp
砚底藏山河1 小时前
python量化:实时行情WebSocket断线重连状态机
java·python·websocket·金融
Yiran_G1 小时前
双频 Wi-Fi 6 + 蓝牙 5.4:无声讯通 WS8712 模组的开发与集成要点
网络·嵌入式硬件·物联网·智能家居
衍生星球1 小时前
【Java 语言】— 方法(方法定义、调用、参数、返回值、方法重载)
java·开发语言·方法
Be for thing2 小时前
PyTorch自动求导Autograd详解|requires_grad、backward、grad、梯度清零
pytorch·学习·1024程序员节