百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术分类 > 正文

手把手教你搞定菜单权限设计,精确到按钮级别,建议收藏

ztj100 2024-10-28 21:12 290 浏览 0 评论

每天早上七点三十,准时推送干货

一、介绍

在实际的项目开发过程中,菜单权限功能可以说是后端管理系统中必不可少的一个环节,根据业务的复杂度,设计的时候可深可浅,但无论怎么变化,设计的思路基本都是围绕着用户、角色、菜单进行相应的扩展

今天小编就和大家一起来讨论一下,怎么设计一套可以精确到按钮级别的菜单权限功能,废话不多说,直接开撸!

二、数据库设计

先来看一下,用户、角色、菜单表对应的ER图,如下:

其中,用户和角色是多对多的关系角色与菜单也是多对多的关系用户通过角色来关联到菜单,当然也有的业务系统菜单权限模型,是可以直接通过用户关联到菜单,对菜单权限可以直接控制到用户级别,不过这个都不是问题,这个也可以进行扩展。

对于用户、角色表比较简单,下面,我们重点来看看菜单表的设计,如下:

可以看到,整个菜单表就是一个树型结构,关键字段说明

  • menu_code:菜单编码,用于后端权限控制

  • parent_id:菜单父节点ID,方便递归遍历菜单

  • node_type:节点类型,可以是文件夹、页面或者按钮类型

  • link_url:页面对应的地址,如果是文件夹或者按钮类型,可以为空

  • level:菜单树的层次,以便于查询指定层级的菜单

  • path:树id的路径,主要用于存放从根节点到当前树的父节点的路径,逗号分隔,想要找父节点会特别快

为了后面方便开发,我们先创建一个名为menu_auth_db的数据库,初始脚本如下:

CREATE DATABASE IF NOT EXISTS menu_auth_db default charset utf8mb4 COLLATE utf8mb4_unicode_ci;

CREATE TABLE menu_auth_db.tb_user (
id bigint(20) unsigned NOT COMMENT '消息给过来的ID',
mobile varchar(20) NOT DEFAULT '' COMMENT '手机号',
name varchar(100) NOT DEFAULT '' COMMENT '姓名',
password varchar(128) NOT DEFAULT '' COMMENT '密码',
is_delete tinyint(4) NOT DEFAULT '0' COMMENT '是否删除 1:已删除;0:未删除',
PRIMARY KEY (id),
KEY idx_name (name) USING BTREE,
KEY idx_mobile (mobile) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户表';

CREATE TABLE menu_auth_db.tb_user_role (
id bigint(20) unsigned NOT COMMENT '主键',
user_id bigint(20) NOT COMMENT '用户ID',
role_id bigint(20) NOT COMMENT '角色ID',
PRIMARY KEY (id),
KEY idx_user_id (user_id) USING BTREE,
KEY idx_role_id (role_id) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户角色表';

CREATE TABLE menu_auth_db.tb_role (
id bigint(20) unsigned NOT COMMENT '主键',
code varchar(100) NOT DEFAULT '' COMMENT '编码',
name varchar(100) NOT DEFAULT '' COMMENT '名称',
is_delete tinyint(4) NOT DEFAULT '0' COMMENT '是否删除 1:已删除;0:未删除',
PRIMARY KEY (id),
KEY idx_code (code) USING BTREE,
KEY idx_name (name) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='角色表';


CREATE TABLE menu_auth_db.tb_role_menu (
id bigint(20) unsigned NOT COMMENT '主键',
role_id bigint(20) NOT COMMENT '角色ID',
menu_id bigint(20) NOT COMMENT '菜单ID',
PRIMARY KEY (id),
KEY idx_role_id (role_id) USING BTREE,
KEY idx_menu_id (menu_id) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='角色菜单关系表';


CREATE TABLE menu_auth_db.tb_menu (
id bigint(20) NOT COMMENT '主键',
name varchar(100) COLLATE utf8mb4_unicode_ci NOT DEFAULT '' COMMENT '名称',
menu_code varchar(100) COLLATE utf8mb4_unicode_ci NOT DEFAULT '' COMMENT '菜单编码',
parent_id bigint(20) DEFAULT COMMENT '父节点',
node_type tinyint(4) NOT DEFAULT '1' COMMENT '节点类型,1文件夹,2页面,3按钮',
icon_url varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT '' COMMENT '图标地址',
sort int(11) NOT DEFAULT '1' COMMENT '排序号',
link_url varchar(500) COLLATE utf8mb4_unicode_ci DEFAULT '' COMMENT '页面对应的地址',
level int(11) NOT DEFAULT '0' COMMENT '层次',
path varchar(2500) COLLATE utf8mb4_unicode_ci DEFAULT '' COMMENT '树id的路径 整个层次上的路径id,逗号分隔,想要找父节点特别快',
is_delete tinyint(4) NOT DEFAULT '0' COMMENT '是否删除 1:已删除;0:未删除',
PRIMARY KEY (id) USING BTREE,
KEY idx_parent_id (parent_id) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='菜单表';

三、后端开发

菜单权限模块的数据库设计,一般5张表就可以搞定,真正有点复杂的地方在于数据的写入和渲染,当然如果老板突然让你来开发一套菜单权限系统,我们也没必要慌张,下面,我们一起来看看后端应该如何开发。

3.1、创建项目

为了方便快捷,小编我采用的是springboot+mybatisPlus组件来快速开发,直接利用mybatisPlus官方提供的快速生成代码的demo,一键生成所需的daoserviceweb层的代码,结果如下:

3.2、编写菜单添加服务

@Override
public void addMenu(Menu menu) {
//如果插入的当前节点为根节点,parentId指定为0
if(menu.getParentId.longValue == 0){
menu.setLevel(1);//根节点层级为1
menu.setPath;//根节点路径为空
}else{
Menu parentMenu = baseMapper.selectById(menu.getParentId);
if(parentMenu == ){
throw new CommonException("未查询到对应的父节点");
}
menu.setLevel(parentMenu.getLevel.intValue + 1);
if(StringUtils.isNotEmpty(parentMenu.getPath)){
menu.setPath(parentMenu.getPath + "," + parentMenu.getId);
}else{
menu.setPath(parentMenu.getId.toString);
}
}
//可以使用雪花算法,生成ID
menu.setId(System.currentTimeMillis);
super.save(menu);
}

新增菜单比较简单,直接将数据插入即可,需要注意的地方是parent_idlevelpath,这三个字段的写入,如果新建的是根节点,默认parent_id0,方便后续递归遍历。

3.3、编写菜单后端查询服务

  • 新建一个菜单视图实体类

@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
public class MenuVo implements Serializable {

private static final long serialVersionUID = -4559267810907997111L;

/**
* 主键
*/
private Long id;

/**
* 名称
*/
private String name;

/**
* 菜单编码
*/
private String menuCode;

/**
* 父节点
*/
private Long parentId;

/**
* 节点类型,1文件夹,2页面,3按钮
*/
private Integer nodeType;

/**
* 图标地址
*/
private String iconUrl;

/**
* 排序号
*/
private Integer sort;

/**
* 页面对应的地址
*/
private String linkUrl;

/**
* 层次
*/
private Integer level;

/**
* 树id的路径 整个层次上的路径id,逗号分隔,想要找父节点特别快
*/
private String path;

/**
* 子菜单集合
*/
List<MenuVo> childMenu;
}
  • 编写菜单查询服务,使用递归重新封装菜单视图

@Override
public List<MenuVo> queryMenuTree {
Wrapper queryObj = new QueryWrapper<>.orderByAsc("level","sort");
List<Menu> allMenu = super.list(queryObj);
// 0L:表示根节点的父ID
List<MenuVo> resultList = transferMenuVo(allMenu, 0L);
return resultList;
}
/**
* 封装菜单视图
* @param allMenu
* @param parentId
* @return
*/
private List<MenuVo> transferMenuVo(List<Menu> allMenu, Long parentId){
List<MenuVo> resultList = new ArrayList<>;
if(!CollectionUtils.isEmpty(allMenu)){
for (Menu source : allMenu) {
if(parentId.longValue == source.getParentId.longValue){
MenuVo menuVo = new MenuVo;
BeanUtils.copyProperties(source, menuVo);
//递归查询子菜单,并封装信息
List<MenuVo> childList = transferMenuVo(allMenu, source.getId);
if(!CollectionUtils.isEmpty(childList)){
menuVo.setChildMenu(childList);
}
resultList.add(menuVo);
}
}
}
return resultList;
}
  • 编写一个菜单树查询接口,如下:

@RestController
@RequestMapping("/menu")
public class MenuController {

@Autowired
private MenuService menuService;

@PostMapping(value = "/queryMenuTree")
public List<MenuVo> queryTreeMenu{
return menuService.queryMenuTree;
}
}

为了便于演示,我们先初始化7条数据,如下图:

其中最后三条是按钮类型,等下会用于后端权限控制,接口查询结果如下:

这个服务是针对后端管理界面查询的,会将所有的菜单全部查询出来以便于进行管理,展示结果类似如下图:

这个图片截图于小编正在开发的一个项目,内容可能不一致,但是数据结构基本都是一致的

3.4、编写用户菜单权限查询服务

在上面,我们介绍到了用户通过角色来关联菜单,因此,很容易想到,流程如下:

  • 第一步:先通过用户查询到对应的角色;

  • 第二步:然后再通过角色查询到对应的菜单;

  • 第三步:最后将菜单查询出来之后进行渲染;

实现过程相比菜单查询服务多了前2个步骤,过程如下:

@Override
public List<MenuVo> queryMenus(Long userId) {
//1、先查询当前用户对应的角色
Wrapper queryUserRoleObj = new QueryWrapper<>.eq("user_id", userId);
List<UserRole> userRoles = userRoleService.list(queryUserRoleObj);
if(!CollectionUtils.isEmpty(userRoles)){
//2、通过角色查询菜单(默认取第一个角色)
Wrapper queryRoleMenuObj = new QueryWrapper<>.eq("role_id", userRoles.get(0).getRoleId);
List<RoleMenu> roleMenus = roleMenuService.list(queryRoleMenuObj);
if(!CollectionUtils.isEmpty(roleMenus)){
Set<Long> menuIds = new HashSet<>;
for (RoleMenu roleMenu : roleMenus) {
menuIds.add(roleMenu.getMenuId);
}
//查询对应的菜单
Wrapper queryMenuObj = new QueryWrapper<>.in("id", new ArrayList<>(menuIds));
List<Menu> menus = super.list(queryMenuObj);
if(!CollectionUtils.isEmpty(menus)){
//将菜单下对应的父节点也一并全部查询出来
Set<Long> allMenuIds = new HashSet<>;
for (Menu menu : menus) {
allMenuIds.add(menu.getId);
if(StringUtils.isNotEmpty(menu.getPath)){
String pathIds = StringUtils.split(",", menu.getPath);
for (String pathId : pathIds) {
allMenuIds.add(Long.valueOf(pathId));
}
}
}
//3、查询对应的所有菜单,并进行封装展示
List<Menu> allMenus = super.list(new QueryWrapper<Menu>.in("id", new ArrayList<>(allMenuIds)));
List<MenuVo> resultList = transferMenuVo(allMenus, 0L);
return resultList;
}
}

}
return ;
}
  • 编写一个用户菜单查询接口,如下:

@PostMapping(value = "/queryMenus")
public List<MenuVo> queryMenus(Long userId){
//查询当前用户下的菜单权限
return menuService.queryMenus(userId);
}

有的同学,可能觉得没必要存放path这个字段,的确在某些场景下不需要。

为什么要存放这个字段呢?

小编在跟前端进行对接的时候,发现这么一个问题,有些前端的树型组件,在勾选子集的时候,不会将对应的父ID传给后端,例如,我在勾选【列表查询】的时候,前端无法将父节点【菜单管理】ID也传给后端,所有后端实际存放的是一个尾节点,需要一个字段path,来存放节点对应的父节点路径。

其实,前端也可以传,只不过需要修改组件的属性,前端修改完成之后,树型组件就无法全选,不满足业务需求。

所以,有些时候得根据实际得情况来进行取舍。

3.5、编写后端权限控制

后端进行权限控制目标,主要是为了防止无权限的用户,进行接口请求查询。

其中菜单编码menuCode就是一个前、后端联系的桥梁,细心的你会发现,所有后端的接口,与前端对应的都是按钮操作,所以我们可以以按钮为基准,实现前后端双向控制。

以【角色管理-查询】这个为例,前端可以通过菜单编码实现是否展示这个查询按钮,后端可以通过菜单编码来判断,当前用户是否具备请求接口的权限。

以后端为例,我们只需编写一个权限注解和代理拦截器即可!

  • 编写一个权限注解

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface CheckPermissions {

String value default "";
}
  • 编写一个代理拦截器,拦截有@CheckPermissions注解的方法

@Aspect
@Component
public class CheckPermissionsAspect {

@Autowired
private MenuMapper menuMapper;

@Pointcut("@annotation(com.company.project.core.annotation.CheckPermissions)")
public void checkPermissions {}

@Before("checkPermissions")
public void doBefore(JoinPoint joinPoint) throws Throwable {
Long userId = ;
Object args = joinPoint.getArgs;
Object parobj = args[0];
//用户请求参数实体类中的用户ID
if(!Objects.is(parobj)){
Class userCla = parobj.getClass;
Field field = userCla.getDeclaredField("userId");
field.setAccessible(true);
userId = (Long) field.get(parobj);
}
if(!Objects.is(userId)){
//获取方法上有CheckPermissions注解的参数
Class clazz = joinPoint.getTarget.getClass;
String methodName = joinPoint.getSignature.getName;
Class parameterTypes = ((MethodSignature)joinPoint.getSignature).getMethod.getParameterTypes;
Method method = clazz.getMethod(methodName, parameterTypes);
if(method.getAnnotation(CheckPermissions.class) != ){
CheckPermissions annotation = method.getAnnotation(CheckPermissions.class);
String menuCode = annotation.value;
if (StringUtils.isNotBlank(menuCode)) {
//通过用户ID、菜单编码查询是否有关联
int count = menuMapper.selectAuthByUserIdAndMenuCode(userId, menuCode);
if(count == 0){
throw new CommonException("接口无访问权限");
}
}
}
}
}
}
  • 我们以【角色管理-查询】为例,先新建一个请求实体类RoleDto,添加用户ID属性

@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
public class RoleDto extends Role {

//添加用户ID
private Long userId;
}
  • 在需要的接口上,添加@CheckPermissions注解,增加权限控制

@RestController
@RequestMapping("/role")
public class RoleController {

private RoleService roleService;

@CheckPermissions(value="roleMgr:list")
@PostMapping(value = "/queryRole")
public List<Role> queryRole(RoleDto roleDto){
return roleService.list;
}

@CheckPermissions(value="roleMgr:add")
@PostMapping(value = "/addRole")
public void addRole(RoleDto roleDto){
roleService.add(roleDto);
}

@CheckPermissions(value="roleMgr:delete")
@PostMapping(value = "/deleteRole")
public void deleteRole(RoleDto roleDto){
roleService.delete(roleDto);
}
}

依次类推,当我们想对某个接口进行权限控制的时候,只需要添加一个注解@CheckPermissions,并填写对应的菜单编码即可!

四、用户权限测试

我们先初始化一个用户【张三】,然后给他分配一个角色【访客人员】,同时给这个角色分配一下2个菜单权限【系统配置】、【用户管理】,等会用于权限测试。

初始内容如下:

数据初始化完成之后,我们来启动项目,传入用户【张三】的ID,查询用户具备的菜单权限,结果如下:

查询结果,用户【张三】有两个菜单权限!

接着,我们来验证一下,用户【张三】是否有角色查询权限,请求角色查询接口如下:

因为没有配置角色查询接口,所以无权访问!

五、总结

整片内容,只介绍了后端关键的服务实现过程,可能也有遗漏的地方,欢迎网友点评、吐槽!

最后说两句(求关注)

最近大家应该发现微信公众号信息流改版了吧,再也不是按照时间顺序展示了。这就对阿粉这样的坚持的原创小号主,可以说非常打击,阅读量直线下降,正反馈持续减弱。

所以看完文章,哥哥姐姐们给阿粉来个在看吧,让阿粉拥有更加大的动力,写出更好的文章,拒绝白嫖,来点正反馈呗~。

如果想在第一时间收到阿粉的文章,不被公号的信息流影响,那么可以给Java极客技术设为一个星标

最后感谢各位的阅读,才疏学浅,难免存在纰漏,如果你发现错误的地方,由于本号没有留言功能,还请你在后台留言指出,我对其加以修改。

最后谢谢大家支持~

最最后,重要的事再说一篇~

快来关注我呀~

快来关注我呀~

快来关注我呀~

< END >

也欢迎大家热爱技术和学习的朋友加入的我们的知识星球当中,我们共同成长,进步。

相关推荐

Python 操作excel的坑__真实的行和列

大佬给的建议__如何快速处理excelopenpyxl库操作excel的时候,单个表的数据量大一些处理速度还能接受,如果涉及多个表甚至多个excel文件的时候速度会很慢,还是建议用pandas来处理,...

Python os.path模块使用指南:轻松处理文件路径

前言在Python编程中,文件和目录的操作是非常重要的一部分。为了方便用户进行文件和目录的操作,Python标准库提供了os模块。其中,os.path子模块提供了一些处理文件路径的函数和方法。本文主要...

Python常用内置模块介绍——文件与系统操作详解

Python提供了多个强大的内置模块用于文件和系统操作,下面我将详细介绍最常用的几个模块及其核心功能。1.os模块-操作系统交互...

Python Flask 建站框架实操教程(flask框架网页)

下面我将带您从零开始构建一个完整的Flask网站,包含用户认证、数据库操作和前端模板等核心功能。##第一部分:基础项目搭建###1.创建项目环境```bash...

为你的python程序上锁:软件序列号生成器

序列号很多同学可能开发了非常多的程序了,并且进行了...

PO设计模式全攻略,在 UI 自动化中的实践总结(以企业微信为例)

一、什么是PO设计模式?PO(PageObject)设计模式将某个页面的所有元素对象定位和对元素对象的操作封装成一个Page类,即一个py文件,并以页面为单位来写测试用例,实现页面对象和测试用例的...

这种小工具居然也能在某鱼卖钱?我用Python一天能写...

前两天在某鱼闲逛,本来想找个二手机械键盘,结果刷着刷着突然看到有人在卖——Word批量转PDF小工具...

python打包成exe,程序有图标,但是任务栏和窗口都没有显示图标

代码中指定图标信息#设置应用ID,确保任务栏图标正确显示ifsys.platform=="win32":importctypesapp_id=...

使用Python构建电影推荐系统(用python做推荐系统)

在日常数据挖掘工作中,除了会涉及到使用Python处理分类或预测任务,有时候还会涉及推荐系统相关任务。...

python爬取并分析淘宝商品信息(python爬取淘宝商品数据)

python爬取并分析淘宝商品信息背景介绍一、模拟登陆二、爬取商品信息1.定义相关参数2.分析并定义正则3.数据爬取三、简单数据分析1.导入库2.中文显示3.读取数据4.分析价格分布5.分析销售...

OpenCV入门学习基础教程(从小白变大神)

Opencv是用于快速处理图像处理、计算机视觉问题的工具,支持多种语言进行开发如c++、python、java等,下面这篇文章主要给大家介绍了关于openCV入门学习基础教程的相关资料,需要的朋友可以...

python图像处理-一行代码实现灰度图抠图

抠图是ps的最基本技能,利用python可以实现用一行代码实现灰度图抠图。基础算法是...

从头开始学python:如何用Matplotlib绘图表

Matplotlib是一个用于绘制图表的库。如果你有用过python处理数据,那Matplotlib可以更直观的帮你把数据展示出来。直接上代码看例子:importmatplotlib.pyplot...

Python爬取爱奇艺腾讯视频 250,000 条数据分析为什么李诞不值得了

在《Python爬取爱奇艺52432条数据分析谁才是《奇葩说》的焦点人物?》这篇文章中,我们从爱奇艺爬取了5万多条评论数据,并对一些关键数据进行了分析,由此总结出了一些明面上看不到的数据,并...

Python Matplotlib 库使用基本指南

简介Matplotlib是一个广泛使用的Python数据可视化库,它可以创建各种类型的图表、图形和可视化效果。无论是简单的折线图还是复杂的热力图,Matplotlib提供了丰富的功能来满足我们...

取消回复欢迎 发表评论: