当前位置: 首页 > 科技观察

教你如何设计菜单权限,精确到按钮级别,推荐收藏

时间:2023-03-17 21:37:48 科技观察

1.简介在实际项目开发过程中,菜单权限功能可以说是后台管理不可或缺的一部分系统。根据业务的复杂度,设计时可深可浅,但无论怎么变,设计思路基本上都是围绕着用户、角色、菜单展开相应的展开。今天,小编就来和大家一起探讨一下,如何设计一套可以精确到按钮级别的菜单权限功能。话不多说,直接打开!2、我们先来看看数据库的设计。ER图如下:其中,用户和角色的关系是多对多的,角色和菜单的关系也是多对多的。用户通过角色与菜单相关联。与菜单相关联,可以直接将菜单权限控制到用户级别,但这不是问题,这个也可以扩展。用户和角色表相对简单。接下来重点介绍菜单表的设计,如下:可以看到,整个菜单表是一个树形结构,关键字段解释:menu_code:菜单代码,用于后端权限控制parent_id:菜单父节点的ID,方便递归遍历菜单,可以为空level:菜单树的层级,为了查询指定层级的菜单路径:树id的路径,主要用来存放当前根节点到父节点的路径树,用逗号隔开,查找父节点会很快。Fortheconvenienceoflaterdevelopment,wefirstcreate一个名为menu_auth_db的数据库,初始脚本如下:CREATEDATABASEIFNOTEXISTSmenu_auth_dbdefaultcharsetutf8mb4COLLATEutf8mb4_unicode_ci;CREATETABLEmenu_auth_db.tb_user(idbigint(20)unsignedNOTNULLCOMMENT'消息给过来的ID',mobilevarchar(20)NOTNULLDEFAULT''COMMENT'手机号',namevarchar(100)NOTNULLDEFAULT''COMMENT'name',passwordvarchar(128)NOTNULLDEFAULT''COMMENT'密码',is_deletetinyint(4)NOTNULLDEFAULT'0'COMMENT'被删除1:删除;0:未删除',PRIMARYKEY(id),keyidx_name(name)使用btree,keyidx_mobile(mobile)使用btree)egine=innodbdefeaultchareet=utf8mb4collat??e=utf8mb4_unicode_cicomment='';',role_idbigint(20)NOTNULL评论'角色ID',primarykey(id),keyIdx_user_id(user_id)使用btree,keyidx_role_id(prole_id)使用btree)egine=innodbdefeaultcharse=utf8mb4collat??e=utf8mb4collat??e=utf8mb4_unicode_cicommentcodevarchar(100)NOTNULLDEFAULT''COMMENT'encoding',namevarchar(100)NOTNULLDEFAULT''COMMENT'name',is_deletetinyint(4)NOTNULLDEFAULT'0'COMMENT'被删除1:删除;0:未删除',PRIMARYKEY(id),keyIdx_code(代码)使用btree,keyidx_name(name)使用btree)egine=innodbdefaultcharset=utf8mb4collat??e=utf8mb4b4b4b4b4cicomment='un_uniCode_un_un_'角色(20)使用btree,keyidx_role_id(reo_id)的notNullComment'菜单ID',keyidx_role_id(reo_id),keyidx_menu_id(菜单_id)使用bbtree)egine=innodbdefeault=utf8mb4collat??e=utf8mb4collat??e=utf8mb4_unicode_cicode_cicod_cicig_cicment='NULLCOMMENT'主键',namevarchar(100)COLLATEutf8mb4_unicode_ciNOTNULLDEFAULT''COMMENT'NAME',menu_codevarchar(100)COLLATEutf8mb4_unicode_ciNOTNULLDEFAULT''COMMENT'菜单代码',parent_idbigint(20)DEFAULTNULLCOMMENT'父节点',node_typetinyint(4)NOTNULDEFAULT'1'COMMENT'节点类型,1个文件夹,2页,3buttons',icon_urlvarchar(255)COLLATEutf8mb4_unicode_ciDEFAULT''COMMENT'图标地址',sortint(11)NOTNULLDEFAULT'1'COMMENT'排序编号',link_urlvarchar(500)COLLATEutf8mb4_unicode_ciDEFAULT''COMMENT'页面对应地址',levelint(11)NOTNULLDEFAULT'0'COMMENT'level',pathvarchar(2500)COLLATEutf8mb4_unicode_ciDEFAULT''COMMENT'treeid'spathidontheentirelevelofpath,用逗号分隔,找到父节点非常快',is_deletetinyint(4)NOTNULLDEFAULT'0'COMMENT'删除1:删除;0:未删除',PRIMARYKEY(id)USINGBTREE,KEYidx_parent_id(parent_id)USINGBTREE)ENGINE=InnoDBDEFAULTCHARSET=utf8mb4COLLATE=utf8mb4_unicode_ciCOMMENT='菜单表';3.后台开发菜单权限模块的数据库设计,一般5个表就可以搞定。真正复杂的部分是数据的写入和渲染。当然,如果老板突然让你开发菜单权限系统,我们也不必慌张。接下来,我们来看看后端应该如何开发3.1.为了方便快捷的创建项目,我使用springboot+mybatisPlus组件进行快速开发。我直接使用mybatisPlus提供的demo快速生成代码,一键生成需要的dao、service、web层代码。结果如下:3.2、writemenuaddservice@OverridepublicvoidaddMenu(Menumenu){//如果当前插入的节点是根节点,则指定parentId为0if(menu.getParentId().longValue()==0){menu.setLevel(1);//根节点级别为1menu.setPath(null);//根节点路径为空}else{MenuparentMenu=baseMapper.selectById(menu.getParentId());if(parentMenu==null){thrownewCommonException("没有查询到对应的父节点");}menu.setLevel(parentMenu.getLevel().intValue()+1);if(StringUtils.isNotEmpty(parentMenu.getPath())){menu.setPath(parentMenu.getPath()+","+parentMenu.getId());}else{menu.setPath(parentMenu.getId().toString());}}//可以使用雪花算法生成IDmenu.setId(System.currentTimeMillis());super.save(menu);}添加新菜单比较简单,直接插入数据即可,注意的地方是parent_id,level,path,这三个字段都要写,如果新建的是根节点,默认parent_id为0,方便后续递归遍历。3.3.写一个菜单后端查询服务新建一个菜单视图实体类@Data@EqualsAndHashCode(callSuper=false)@Accessors(chain=true)publicclassMenuVoimplementsSerializable{privatestaticfinallongserialVersionUID=-4559267810907997111L;/***主键*/privateLongid;/***名称*/privateStringname;/***菜单代码*/privateStringmenuCode;/***父节点*/privateLongparentId;/***节点类型,1文件夹,2页面,3按钮*/privateIntegernodeType;/***图标地址*/privateStringiconUrl;/***sortnumber*/privateIntegersort;/***页面对应地址*/privateStringlinkUrl;/***level*/privateIntegerlevel;/***整层树id路径上的路径id,逗号分隔,我想很快找到父节点*/privateStringpath;/***子菜单集合*/ListchildMenu;}写一个菜单查询服务,使用递归重新封装菜单视图@OverridepublicListqueryMenuTree(){WrapperqueryObj=newQueryWrapper<>().orderByAsc("level","sort");List

allMenu=super.list(queryObj);//0L:表示根节点的父IDallMenu,0L);returnresultList;}/***封装菜单视图*@paramallMenu*@paramparentId*@return*/privateListtransferMenuVo(ListallMenu,LongparentId){ListresultList=newArrayList<>();if(!CollectionUtils.isEmpty(allMenu)){for(Menusource:allMenu){if(parentId.longValue()==source.getParentId().longValue()){MenuVomenuVo=newMenuVo();BeanUtils.copyProperties(source,menuVo);//递归查询子菜单并封装信息ListchildList=transferMenuVo(allMenu,source.getId());if(!CollectionUtils.isEmpty(childList)){menuVo.setChildMenu(childList);}resultList.add(menuVo);}}}returnresultList;}写一个菜单树查询接口如下:@RestController@RequestMapping("/menu")publicclassMenuController{@AutowiredprivateMenuServicemenuService;@PostMapping(value="/queryMenuTree")publicListqueryTreeMenu(){returnmenuService.queryMenuTree();}}为了演示方便,我们先初始化7条数据,如下图:后三个为按钮类型,后面用于后台权限控制,接口查询结果如下:Thisse后台管理界面查询rvice,所有菜单都会查询,方便管理。显示结果类似下图:这张图是小编在开发的一个项目中截图的,内容可能不一致,但是数据结构基本一致3.4.编写用户菜单权限查询服务上面我们介绍了用户通过角色关联菜单,那么很容易想象流程如下:第一步:首先通过用户查询对应的角色;Step2:然后通过角色查询对应的菜单;第三步:最后查询菜单并渲染;实现过程比菜单查询服务多了两步,流程如下:@OverridepublicListqueryMenus(LonguserId){//1,先查询当前用户对应的角色WrapperqueryUserRoleObj=newQueryWrapper<>().eq("user_id",userId);ListuserRoles=userRoleService.list(queryUserRoleObj);if(!CollectionUtils.isEmpty(userRoles)){//2.按角色查询菜单(默认取第一个角色)WrapperqueryRoleMenuObj=newQueryWrapper<>().eq("role_id",userRoles.get(0).getRoleId());ListroleMenus=roleMenuService.list(queryRoleMenuObj);if(!CollectionUtils.isEmpty(roleMenus)){SetmenuIds=newHashSet<>();for(RoleMenuroleMenu:roleMenus){menuIds.add(roleMenu.getMenuId());}//查询对应MenuWrapperqueryMenuObj=newQueryWrapper<>().in("id",newArrayList<>(menuIds));Listmenus=super.list(queryMenuObj);if(!CollectionUtils.isEmpty(menus)){//查询菜单下所有对应的父节点输出SetallMenuIds=newHashSet<>();for(Menumenu:menus){allMenuIds.add(menu.getId());if(StringUtils.isNotEmpty(menu.getPath())){String[]pathIds=StringUtils.split(",",menu.getPath());for(StringpathId:pathIds){allMenuIds.add(Long.valueOf(pathId));}}}//3.查询所有对应的菜单并封装ShowListallMenus=super.list(newQueryWrapper().in("id",newArrayList<>(allMenuIds)));ListresultList=transferMenuVo(allMenus,0L);returnresultList;}}}returnnull;}写一个用户菜单查询接口,如下:@PostMapping(value="/queryMenus")publicListqueryMenus(LonguserId){//查询当前用户下的菜单权限returnmenuService.queryMenus(userId);}Yes同学们可能会认为没有必要存储path字段。确实,为什么有些场景需要存储这个字段呢?我在和前端连接的时候,发现了这样一个问题。一些前端树组件在选择子集时,不会将对应的parentID传递给后端。比如我勾选【列表查询】,前端无法将父节点【菜单管理】的ID传给后端。终端实际存储了一个尾节点,需要一个字段path来存储该节点对应的父节点的路径。其实前端也可以上传,只是需要修改组件的属性。前端修改完成后,无法选中所有的树组件,不符合业务需求。因此,有时我们不得不根据实际情况做出取舍。3.5.写后端权限控制后端权限控制的目的是防止没有权限的用户查询接口。其中,菜单代码menuCode是前后端之间的桥梁。细心的你会发现,所有的后端界面都对应着前端按钮的操作,所以我们可以以按钮为基准,实现前后端的双向控制。以【角色管理-查询】为例,前端可以通过菜单代码实现是否显示查询按钮,后端可以通过菜单代码判断当前用户是否有权限请求该接口.以后端为例,我们只需要写一个权限注解和代理拦截器即可!写一个权限注解@Target({ElementType.TYPE,ElementType.METHOD})@Retention(RetentionPolicy.RUNTIME)public@interfaceCheckPermissions{Stringvalue()default"";}写一个代理拦截器拦截@CheckPermissions@Aspect@注解的方法ComponentpublicclassCheckPermissionsAspect{@AutowiredprivateMenuMappermenuMapper;@Pointcut("@annotation(com.company.project.core.annotation.CheckPermissions)")publicvoidcheckPermissions(){}@Before("checkPermissions()")publicvoiddoBefore(JoinPointjoinPoint)throwsThrowable{LonguserId=null;Object[]args=joinPoint.getArgs();Objectparobj=args[0];//用户请求参数实体类中的UserIDif(!Objects.isNull(parobj)){ClassuserCla=parobj.getClass();Fieldfield=userCla.getDeclaredField("userId");field.setAccessible(true);userId=(Long)field.get(parobj);}if(!Objects.isNull(userId)){//获取带有CheckPermissions注解的参数方法Classclazz=joinPoint.getTarget().getClass();StringmethodName=joinPoint.getSignature().getName();类[]参数类型=((MethodSignature)joinPoint.getSignature()).getMethod().getParameterTypes();Methodmethod=clazz.getMethod(methodName,parameterTypes);if(method.getAnnotation(CheckPermissions.class)!=null){CheckPermissionsannotation=方法。getAnnotation(CheckPermissions.class);StringmenuCode=annotation.value();if(StringUtils.isNotBlank(menuCode)){//通过用户ID和菜单码查询是否有关系intcount=menuMapper.selectAuthByUserIdAndMenuCode(userId,menuCode);if(count==0){thrownewCommonException("该接口没有访问权限");}}}}}}我们以【角色管理-查询】为例,首先新建一个请求实体类RoleDto,添加用户ID属性@Data@EqualsAndHashCode(callSuper=false)@Accessors(chain=true)publicclassRoleDtoextendsRole{//添加用户IDprivateLonguserId;}在需要的接口上添加@CheckPermissions注解增加权限控制@RestController@RequestMapping("/role")publicclassRoleController{privateRoleServiceroleService;@CheckPermissions(value="roleMgr:list")@PostMapping(value="/queryRole")publicListqueryRole(RoleDtoroleDto){returnroleService.list();}@CheckPermissions(value="roleMgr:add")@PostMapping(value="/addRole")publicvoidaddRole(RoleDtoroleDto){roleService.add(roleDto);}@CheckPermissions(value="roleMgr:delete")@PostMapping(值="/deleteRole")publicvoiddeleteRole(RoleDtoroleDto){roleService.delete(roleDto);}}等,当我们要控制某个接口的权限时,只需要添加一个注解@CheckPermissions,并填写相应的即可菜单代码!4。用户权限测试我们先初始化一个用户【张三】,然后给他分配一个角色【访客】,并为这个角色分配2个菜单权限【系统配置】和【用户管理】,后面会用到初始内容权限测试如下:数据初始化完成后,我们启动项目,传入用户【张三】的ID,查询用户的菜单权限。结果如下:查询结果,用户【张三】有两个菜单权限!接下来我们来验证用户【张三】是否有角色查询权限。请求角色查询界面如下:因为没有配置角色查询界面,所以没有访问权限!服务实现过程中可能会有一些遗漏。欢迎广大网友评论投诉!

最新推荐
猜你喜欢