Java菜单权限案例如何实现

wen java案例 26

Java菜单权限案例如何实现:从零搭建企业级RBAC权限模型

目录导读

  1. 为什么你需要关注菜单权限?
  2. 核心概念:RBAC模型与菜单树
  3. 数据库表设计案例
  4. 后端实现:Spring Security + 自定义过滤器
  5. 前端动态渲染:Vue/React的路由与菜单联动
  6. 常见问题与问答
  7. 总结与最佳实践

为什么你需要菜单权限?

在现代企业级应用中,菜单权限不仅是“看到或看不到某个按钮”那么简单,它直接关系到数据安全、操作合规与用户体验,试想一个电商后台:普通运营人员只能看到订单管理、商品上架菜单,而财务人员才能看到结算对账模块,如果权限混乱,轻则操作失误,重则数据泄露。

Java菜单权限案例如何实现

问:菜单权限与接口权限有何区别?
答:菜单权限控制前端UI的可见性,防止用户点击未授权功能;接口权限控制后端API的访问,防止直接调用未授权接口,两者必须配合实现,缺一不可,比如即使前端隐藏了“删除用户”按钮,若后端接口未校验,用户仍可通过Postman删除数据。

核心概念:RBAC模型与菜单树

Java领域最成熟的权限模型是RBAC(Role-Based Access Control,基于角色的权限控制),核心三要素:用户、角色、权限,而菜单权限本质是“资源权限”的一种可视化表现形式。

菜单通常以树形结构存储(如父菜单→子菜单→按钮),权限标识符推荐使用“模块:功能:操作”的命名方式,

  • system:user:list(查看用户列表)
  • system:user:delete(删除用户)
  • order:export(导出订单)

关键关系:一个角色拥有多个权限,一个用户拥有多个角色,菜单权限校验时,先查用户角色,再查角色绑定的权限集合,最后匹配当前页面需要的权限标识。

数据库表设计案例

以下是最实用的5张核心表(MySQL示例):

-- 用户表
CREATE TABLE `sys_user` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `username` varchar(50) NOT NULL,
  `password` varchar(100) NOT NULL,
  PRIMARY KEY (`id`)
);
-- 角色表
CREATE TABLE `sys_role` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `role_name` varchar(50) NOT NULL,
  `role_key` varchar(50) NOT NULL COMMENT '角色标识如 admin, normal',
  PRIMARY KEY (`id`)
);
-- 菜单表(包含按钮)
CREATE TABLE `sys_menu` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `parent_id` bigint(20) DEFAULT 0,
  `name` varchar(100) NOT NULL COMMENT '菜单名称',
  `path` varchar(255) DEFAULT NULL COMMENT '路由地址',
  `permission` varchar(100) DEFAULT NULL COMMENT '权限标识如 system:user:add',
  `type` char(1) DEFAULT 'M' COMMENT 'M菜单 C按钮',
  `sort` int(4) DEFAULT 0,
  PRIMARY KEY (`id`)
);
-- 用户角色关联表
CREATE TABLE `sys_user_role` (
  `user_id` bigint(20) NOT NULL,
  `role_id` bigint(20) NOT NULL,
  PRIMARY KEY (`user_id`,`role_id`)
);
-- 角色菜单关联表
CREATE TABLE `sys_role_menu` (
  `role_id` bigint(20) NOT NULL,
  `menu_id` bigint(20) NOT NULL,
  PRIMARY KEY (`role_id`,`menu_id`)
);

问:菜单表和权限表为何合二为一?
答:企业级项目中通常将菜单项和按钮级操作统一视为“资源”,菜单项使用 permission 字段记录其需要的权限标识,这样前端渲染菜单时即可直接校验权限,无需额外查询,若需更细粒度控制,可独立出 sys_permission 表。

后端实现:Spring Security + 自定义过滤器

以Spring Boot + Spring Security为例,关键步骤:

1 加载用户权限

在UserDetailsService实现中,查询用户角色及关联的菜单权限标识集合:

@Override
public UserDetails loadUserByUsername(String username) {
    // 1. 查询用户
    // 2. 查询角色
    // 3. 通过角色查询所有菜单的permission字段
    List<String> permissions = menuMapper.selectPermissionByUserId(userId);
    // 4. 构造SecurityUser对象
    return new SecurityUser(user, permissions);
}

2 动态菜单接口(核心)

为前端提供“该用户可见的菜单树”接口:

@GetMapping("/getRouters")
public Result<List<MenuVO>> getRouters() {
    // 获取当前用户的所有菜单(过滤掉未授权的)
    List<Menu> menus = menuService.selectMenusByUserId(getUserId());
    // 转为树形结构(递归处理parentId)
    return Result.success(buildTree(menus));
}

3 接口权限校验

使用Spring Security的 @PreAuthorize 注解在Controller方法上:

@PreAuthorize("@ss.hasPermi('system:user:add')")
@PostMapping("/user")
public Result addUser(@RequestBody User user) { ... }

ss 是自定义权限校验Bean,内部通过当前线程的SecurityContext获取用户的权限集合进行比对。

前端动态渲染:Vue/React的路由与菜单联动

前端核心思路:先获取后端返回的菜单树,再根据菜单树动态生成路由和侧边栏

Vue实现(基于Vue Router + Element UI)

// 1. 登录成功后调用接口获取菜单树
const menus = await fetchMenuTree();
// 2. 递归转化菜单为路由配置
function generateRoutes(menus, basePath = '') {
    return menus.map(menu => {
        const route = {
            path: menu.path,
            component: loadView(menu.component), // 懒加载
            meta: { title: menu.name, permission: menu.permission }
        };
        if (menu.children) {
            route.children = generateRoutes(menu.children, menu.path);
        }
        return route;
    });
}
// 3. 动态添加路由
router.addRoutes(generateRoutes(menus));

React实现(基于React Router + Ant Design Pro)

// 同样先获取菜单列表,然后用递归组件渲染
function MenuTree({ menus, onClick }) {
    return menus.map(menu => (
        <Menu.SubMenu key={menu.id} title={menu.name}
            onClick={() => onClick(menu)}>
            {menu.children?.map(child => (
                <Menu.Item key={child.id}
                    permission={child.permission}>
                    {child.name}
                </Menu.Item>
            ))}
        </Menu.SubMenu>
    ));
}

问:为何不把菜单数据硬编码在前端路由表里?
答:硬编码无法动态根据角色变化,例如给普通运营人员分配了“数据分析”角色,前端若不动态刷新路由,用户永远看不到该菜单入口,必须重新编译或手动修改前端代码,完全违背了权限系统灵活设计的初衷。

常见问题与问答

Q1:后端返回的菜单树包含按钮怎么办?

A:按钮(type='C')的菜单项不应生成路由,仅用于前端按钮级显隐,可在返回前过滤掉 menu.type === 'C',或在树形递归中根据类型决定是否生成路由对象。

Q2:如何实现“上级用户能看到下级用户菜单,反之不行”?

A:这属于“数据权限”而非菜单权限,需要额外设计组织架构表,并在菜单查询时加入 scope 过滤条件,例如只返回管理员创建的菜单或全局菜单。

Q3:缓存了用户权限,修改角色权限后没生效怎么办?

A:常用方案:前端在每次刷新页面或切换Tab时重新请求 /getRouters 接口;后端可在修改角色权限后,清空该角色下所有用户的缓存(如Redis的 permissions:userId 键),下次请求时自动重新加载。

Q4:前端按钮级权限如何精细化控制?

A:定义一个 hasPermission(code) 方法:

// 从store中获取当前用户权限列表
function hasPermission(code) {
    return permissionList.includes(code);
}
// 模板中使用
<el-button v-if="hasPermission('system:user:edit')">编辑</el-button>

总结与最佳实践

实现Java菜单权限系统的核心流程可概括为:后端定义权限标识→数据库存储角色-菜单关联→接口返回动态树→前端路由+按钮联动校验

几点经验建议:

  1. 权限标识避免使用数字ID,改用字符串可读性强,且便于后期拆分微服务;
  2. 前后端双重校验:前端防误操作,后端防恶意调用;
  3. 菜单树缓存到Redis:减少数据库查询,提升页面加载速度;
  4. 版本控制:每次修改菜单或角色权限时,在业务日志中记录操作人及变更内容,方便回溯。

如果你正在搭建自己的权限系统,建议先从“单表权限标识”开始,逐步完善“角色-用户-菜单”的多对多关系,最终再考虑引入AOP切面或注解实现更高级的细粒度控制,只要掌握了本文的结构和核心代码思路,无论前端是Vue还是React,后端是Spring Boot还是国产框架,都能快速适配实现。

抱歉,评论功能暂时关闭!