成人免费xxxxx在线视频软件_久久精品久久久_亚洲国产精品久久久_天天色天天色_亚洲人成一区_欧美一级欧美三级在线观看

手把手教你搞定菜單權限設計,精確到按鈕級別,建議收藏

開發 項目管理
在實際的項目開發過程中,菜單權限功能可以說是后端管理系統中必不可少的一個環節,根據業務的復雜度,設計的時候可深可淺,但無論怎么變化,設計的思路基本都是圍繞著用戶、角色、菜單進行相應的擴展。

 一、介紹

在實際的項目開發過程中,菜單權限功能可以說是后端管理系統中必不可少的一個環節,根據業務的復雜度,設計的時候可深可淺,但無論怎么變化,設計的思路基本都是圍繞著用戶、角色、菜單進行相應的擴展。

[[331842]]

今天小編就和大家一起來討論一下,怎么設計一套可以精確到按鈕級別的菜單權限功能,廢話不多說,直接開擼!

二、數據庫設計

先來看一下,用戶、角色、菜單表對應的ER圖,如下:

 

其中,用戶和角色是多對多的關系,角色與菜單也是多對多的關系,用戶通過角色來關聯到菜單,當然也有的業務系統菜單權限模型,是可以直接通過用戶關聯到菜單,對菜單權限可以直接控制到用戶級別,不過這個都不是問題,這個也可以進行擴展。

對于用戶、角色表比較簡單,下面,我們重點來看看菜單表的設計,如下:

 

可以看到,整個菜單表就是一個樹型結構,關鍵字段說明:

  • menu_code:菜單編碼,用于后端權限控制
  • parent_id:菜單父節點ID,方便遞歸遍歷菜單
  • node_type:節點類型,可以是文件夾、頁面或者按鈕類型
  • link_url:頁面對應的地址,如果是文件夾或者按鈕類型,可以為空
  • level:菜單樹的層次,以便于查詢指定層級的菜單
  • path:樹id的路徑,主要用于存放從根節點到當前樹的父節點的路徑,逗號分隔,想要找父節點會特別快

為了后面方便開發,我們先創建一個名為menu_auth_db的數據庫,初始腳本如下:

  1. CREATE DATABASE IF NOT EXISTS menu_auth_db default charset utf8mb4 COLLATE utf8mb4_unicode_ci; 
  2.  
  3. CREATE TABLE menu_auth_db.tb_user ( 
  4.   id bigint(20) unsigned NOT NULL COMMENT '消息給過來的ID'
  5.   mobile varchar(20) NOT NULL DEFAULT '' COMMENT '手機號'
  6.   name varchar(100) NOT NULL DEFAULT '' COMMENT '姓名'
  7.   password varchar(128) NOT NULL DEFAULT '' COMMENT '密碼'
  8.   is_delete tinyint(4) NOT NULL DEFAULT '0' COMMENT '是否刪除 1:已刪除;0:未刪除'
  9.   PRIMARY KEY (id), 
  10.   KEY idx_name (name) USING BTREE, 
  11.   KEY idx_mobile (mobile) USING BTREE 
  12. ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用戶表'
  13.  
  14. CREATE TABLE menu_auth_db.tb_user_role ( 
  15.   id bigint(20) unsigned NOT NULL COMMENT '主鍵'
  16.   user_id bigint(20) NOT NULL COMMENT '用戶ID'
  17.   role_id bigint(20) NOT NULL COMMENT '角色ID'
  18.   PRIMARY KEY (id), 
  19.   KEY idx_user_id (user_id) USING BTREE, 
  20.   KEY idx_role_id (role_id) USING BTREE 
  21. ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用戶角色表'
  22.  
  23. CREATE TABLE menu_auth_db.tb_role ( 
  24.   id bigint(20) unsigned NOT NULL COMMENT '主鍵'
  25.   code varchar(100) NOT NULL DEFAULT '' COMMENT '編碼'
  26.   name varchar(100) NOT NULL DEFAULT '' COMMENT '名稱'
  27.   is_delete tinyint(4) NOT NULL DEFAULT '0' COMMENT '是否刪除 1:已刪除;0:未刪除'
  28.   PRIMARY KEY (id), 
  29.   KEY idx_code (code) USING BTREE, 
  30.   KEY idx_name (name) USING BTREE 
  31. ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='角色表'
  32.  
  33.  
  34. CREATE TABLE menu_auth_db.tb_role_menu ( 
  35.   id bigint(20) unsigned NOT NULL COMMENT '主鍵'
  36.   role_id bigint(20) NOT NULL COMMENT '角色ID'
  37.   menu_id bigint(20) NOT NULL COMMENT '菜單ID'
  38.   PRIMARY KEY (id), 
  39.   KEY idx_role_id (role_id) USING BTREE, 
  40.   KEY idx_menu_id (menu_id) USING BTREE 
  41. ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='角色菜單關系表'
  42.  
  43.  
  44. CREATE TABLE menu_auth_db.tb_menu ( 
  45.   id bigint(20) NOT NULL COMMENT '主鍵'
  46.   name varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '名稱'
  47.   menu_code varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '菜單編碼'
  48.   parent_id bigint(20) DEFAULT NULL COMMENT '父節點'
  49.   node_type tinyint(4) NOT NULL DEFAULT '1' COMMENT '節點類型,1文件夾,2頁面,3按鈕'
  50.   icon_url varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT '' COMMENT '圖標地址'
  51.   sort int(11) NOT NULL DEFAULT '1' COMMENT '排序號'
  52.   link_url varchar(500) COLLATE utf8mb4_unicode_ci DEFAULT '' COMMENT '頁面對應的地址'
  53.   level int(11) NOT NULL DEFAULT '0' COMMENT '層次'
  54.   path varchar(2500) COLLATE utf8mb4_unicode_ci DEFAULT '' COMMENT '樹id的路徑 整個層次上的路徑id,逗號分隔,想要找父節點特別快'
  55.   is_delete tinyint(4) NOT NULL DEFAULT '0' COMMENT '是否刪除 1:已刪除;0:未刪除'
  56.   PRIMARY KEY (id) USING BTREE, 
  57.   KEY idx_parent_id (parent_id) USING BTREE 
  58. ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='菜單表'

三、后端開發

菜單權限模塊的數據庫設計,一般5張表就可以搞定,真正有點復雜的地方在于數據的寫入和渲染,當然如果老板突然讓你來開發一套菜單權限系統,我們也沒必要慌張,下面,我們一起來看看后端應該如何開發。

3.1、創建項目

為了方便快捷,小編我采用的是springboot+mybatisPlus組件來快速開發,直接利用mybatisPlus官方提供的快速生成代碼的demo,一鍵生成所需的dao、service、web層的代碼,結果如下:

 

3.2、編寫菜單添加服務

  1. @Override 
  2. public void addMenu(Menu menu) { 
  3.     //如果插入的當前節點為根節點,parentId指定為0 
  4.     if(menu.getParentId().longValue() == 0){ 
  5.         menu.setLevel(1);//根節點層級為1 
  6.         menu.setPath(null);//根節點路徑為空 
  7.     }else
  8.         Menu parentMenu = baseMapper.selectById(menu.getParentId()); 
  9.         if(parentMenu == null){ 
  10.             throw new CommonException("未查詢到對應的父節點"); 
  11.         } 
  12.         menu.setLevel(parentMenu.getLevel().intValue() + 1); 
  13.         if(StringUtils.isNotEmpty(parentMenu.getPath())){ 
  14.             menu.setPath(parentMenu.getPath() + "," + parentMenu.getId()); 
  15.         }else
  16.             menu.setPath(parentMenu.getId().toString()); 
  17.         } 
  18.     } 
  19.     //可以使用雪花算法,生成ID 
  20.     menu.setId(System.currentTimeMillis()); 
  21.     super.save(menu); 

新增菜單比較簡單,直接將數據插入即可,需要注意的地方是parent_id、level、path,這三個字段的寫入,如果新建的是根節點,默認parent_id為0,方便后續遞歸遍歷。

3.3、編寫菜單后端查詢服務

  • 新建一個菜單視圖實體類
  1. @Data 
  2. @EqualsAndHashCode(callSuper = false
  3. @Accessors(chain = true
  4. public class MenuVo implements Serializable { 
  5.  
  6.     private static final long serialVersionUID = -4559267810907997111L; 
  7.  
  8.     /** 
  9.      * 主鍵 
  10.      */ 
  11.     private Long id; 
  12.  
  13.     /** 
  14.      * 名稱 
  15.      */ 
  16.     private String name
  17.  
  18.     /** 
  19.      * 菜單編碼 
  20.      */ 
  21.     private String menuCode; 
  22.  
  23.     /** 
  24.      * 父節點 
  25.      */ 
  26.     private Long parentId; 
  27.  
  28.     /** 
  29.      * 節點類型,1文件夾,2頁面,3按鈕 
  30.      */ 
  31.     private Integer nodeType; 
  32.  
  33.     /** 
  34.      * 圖標地址 
  35.      */ 
  36.     private String iconUrl; 
  37.  
  38.     /** 
  39.      * 排序號 
  40.      */ 
  41.     private Integer sort; 
  42.  
  43.     /** 
  44.      * 頁面對應的地址 
  45.      */ 
  46.     private String linkUrl; 
  47.  
  48.     /** 
  49.      * 層次 
  50.      */ 
  51.     private Integer level
  52.  
  53.     /** 
  54.      * 樹id的路徑 整個層次上的路徑id,逗號分隔,想要找父節點特別快 
  55.      */ 
  56.     private String path; 
  57.  
  58.     /** 
  59.      * 子菜單集合 
  60.      */ 
  61.     List<MenuVo> childMenu; 
  • 編寫菜單查詢服務,使用遞歸重新封裝菜單視圖
  1. @Override 
  2. public List<MenuVo> queryMenuTree() { 
  3.     Wrapper queryObj = new QueryWrapper<>().orderByAsc("level","sort"); 
  4.     List<Menu> allMenu = super.list(queryObj); 
  5.  // 0L:表示根節點的父ID 
  6.     List<MenuVo> resultList = transferMenuVo(allMenu, 0L); 
  7.     return resultList; 
  8. /** 
  9.  * 封裝菜單視圖 
  10.  * @param allMenu 
  11.  * @param parentId 
  12.  * @return 
  13.  */ 
  14. private List<MenuVo> transferMenuVo(List<Menu> allMenu, Long parentId){ 
  15.     List<MenuVo> resultList = new ArrayList<>(); 
  16.     if(!CollectionUtils.isEmpty(allMenu)){ 
  17.         for (Menu source : allMenu) { 
  18.             if(parentId.longValue() == source.getParentId().longValue()){ 
  19.                 MenuVo menuVo = new MenuVo(); 
  20.                 BeanUtils.copyProperties(source, menuVo); 
  21.                 //遞歸查詢子菜單,并封裝信息 
  22.                 List<MenuVo> childList = transferMenuVo(allMenu, source.getId()); 
  23.                 if(!CollectionUtils.isEmpty(childList)){ 
  24.                     menuVo.setChildMenu(childList); 
  25.                 } 
  26.                 resultList.add(menuVo); 
  27.             } 
  28.         } 
  29.     } 
  30.     return resultList; 

編寫一個菜單樹查詢接口,如下:

  1. @RestController 
  2. @RequestMapping("/menu"
  3. public class MenuController { 
  4.  
  5.     @Autowired 
  6.     private MenuService menuService; 
  7.  
  8.     @PostMapping(value = "/queryMenuTree"
  9.     public List<MenuVo> queryTreeMenu(){ 
  10.         return menuService.queryMenuTree(); 
  11.     } 

為了便于演示,我們先初始化7條數據,如下圖:

 

其中最后三條是按鈕類型,等下會用于后端權限控制,接口查詢結果如下:

 

這個服務是針對后端管理界面查詢的,會將所有的菜單全部查詢出來以便于進行管理,展示結果類似如下圖:

 

這個圖片截圖于小編正在開發的一個項目,內容可能不一致,但是數據結構基本都是一致的。

3.4、編寫用戶菜單權限查詢服務

在上面,我們介紹到了用戶通過角色來關聯菜單,因此,很容易想到,流程如下:

  • 第一步:先通過用戶查詢到對應的角色;
  • 第二步:然后再通過角色查詢到對應的菜單;
  • 第三步:最后將菜單查詢出來之后進行渲染;

實現過程相比菜單查詢服務多了前2個步驟,過程如下:

  1. @Override 
  2. public List<MenuVo> queryMenus(Long userId) { 
  3.     //1、先查詢當前用戶對應的角色 
  4.     Wrapper queryUserRoleObj = new QueryWrapper<>().eq("user_id", userId); 
  5.     List<UserRole> userRoles = userRoleService.list(queryUserRoleObj); 
  6.     if(!CollectionUtils.isEmpty(userRoles)){ 
  7.         //2、通過角色查詢菜單(默認取第一個角色) 
  8.         Wrapper queryRoleMenuObj = new QueryWrapper<>().eq("role_id", userRoles.get(0).getRoleId()); 
  9.         List<RoleMenu> roleMenus = roleMenuService.list(queryRoleMenuObj); 
  10.         if(!CollectionUtils.isEmpty(roleMenus)){ 
  11.             Set<Long> menuIds = new HashSet<>(); 
  12.             for (RoleMenu roleMenu : roleMenus) { 
  13.                 menuIds.add(roleMenu.getMenuId()); 
  14.             } 
  15.             //查詢對應的菜單 
  16.             Wrapper queryMenuObj = new QueryWrapper<>().in("id", new ArrayList<>(menuIds)); 
  17.             List<Menu> menus = super.list(queryMenuObj); 
  18.             if(!CollectionUtils.isEmpty(menus)){ 
  19.                 //將菜單下對應的父節點也一并全部查詢出來 
  20.                 Set<Long> allMenuIds = new HashSet<>(); 
  21.                 for (Menu menu : menus) { 
  22.                     allMenuIds.add(menu.getId()); 
  23.                     if(StringUtils.isNotEmpty(menu.getPath())){ 
  24.                         String[] pathIds = StringUtils.split(",", menu.getPath()); 
  25.                         for (String pathId : pathIds) { 
  26.                             allMenuIds.add(Long.valueOf(pathId)); 
  27.                         } 
  28.                     } 
  29.                 } 
  30.                 //3、查詢對應的所有菜單,并進行封裝展示 
  31.                 List<Menu> allMenus = super.list(new QueryWrapper<Menu>().in("id", new ArrayList<>(allMenuIds))); 
  32.                 List<MenuVo> resultList = transferMenuVo(allMenus, 0L); 
  33.                 return resultList; 
  34.             } 
  35.         } 
  36.  
  37.     } 
  38.     return null
  • 編寫一個用戶菜單查詢接口,如下:
  1. @PostMapping(value = "/queryMenus"
  2. public List<MenuVo> queryMenus(Long userId){ 
  3.  //查詢當前用戶下的菜單權限 
  4.     return menuService.queryMenus(userId); 

有的同學,可能覺得沒必要存放path這個字段,的確在某些場景下不需要。

為什么要存放這個字段呢?

小編在跟前端進行對接的時候,發現這么一個問題,有些前端的樹型組件,在勾選子集的時候,不會將對應的父ID傳給后端,例如,我在勾選【列表查詢】的時候,前端無法將父節點【菜單管理】ID也傳給后端,所有后端實際存放的是一個尾節點,需要一個字段path,來存放節點對應的父節點路徑。

 

其實,前端也可以傳,只不過需要修改組件的屬性,前端修改完成之后,樹型組件就無法全選,不滿足業務需求。

所以,有些時候得根據實際得情況來進行取舍。

3.5、編寫后端權限控制

后端進行權限控制目標,主要是為了防止無權限的用戶,進行接口請求查詢。

其中菜單編碼menuCode就是一個前、后端聯系的橋梁,細心的你會發現,所有后端的接口,與前端對應的都是按鈕操作,所以我們可以以按鈕為基準,實現前后端雙向控制。

以【角色管理-查詢】這個為例,前端可以通過菜單編碼實現是否展示這個查詢按鈕,后端可以通過菜單編碼來判斷,當前用戶是否具備請求接口的權限。

以后端為例,我們只需編寫一個權限注解和代理攔截器即可!

  • 編寫一個權限注解
  1. @Target({ElementType.TYPE, ElementType.METHOD}) 
  2. @Retention(RetentionPolicy.RUNTIME) 
  3. public @interface CheckPermissions { 
  4.  
  5.     String value() default ""
  • 編寫一個代理攔截器,攔截有@CheckPermissions注解的方法
  1. @Aspect 
  2. @Component 
  3. public class CheckPermissionsAspect { 
  4.  
  5.     @Autowired 
  6.     private MenuMapper menuMapper; 
  7.  
  8.     @Pointcut("@annotation(com.company.project.core.annotation.CheckPermissions)"
  9.     public void checkPermissions() {} 
  10.  
  11.     @Before("checkPermissions()"
  12.     public void doBefore(JoinPoint joinPoint) throws Throwable { 
  13.         Long userId = null
  14.         Object[] args = joinPoint.getArgs(); 
  15.         Object parobj = args[0]; 
  16.         //用戶請求參數實體類中的用戶ID 
  17.         if(!Objects.isNull(parobj)){ 
  18.             Class userCla = parobj.getClass(); 
  19.             Field field = userCla.getDeclaredField("userId"); 
  20.             field.setAccessible(true); 
  21.             userId = (Long) field.get(parobj); 
  22.         } 
  23.         if(!Objects.isNull(userId)){ 
  24.             //獲取方法上有CheckPermissions注解的參數 
  25.             Class clazz = joinPoint.getTarget().getClass(); 
  26.             String methodName = joinPoint.getSignature().getName(); 
  27.             Class[] parameterTypes = ((MethodSignature)joinPoint.getSignature()).getMethod().getParameterTypes(); 
  28.             Method method = clazz.getMethod(methodName, parameterTypes); 
  29.             if(method.getAnnotation(CheckPermissions.class) != null){ 
  30.                 CheckPermissions annotation = method.getAnnotation(CheckPermissions.class); 
  31.                 String menuCode = annotation.value(); 
  32.                 if (StringUtils.isNotBlank(menuCode)) { 
  33.                     //通過用戶ID、菜單編碼查詢是否有關聯 
  34.                     int count = menuMapper.selectAuthByUserIdAndMenuCode(userId, menuCode); 
  35.                     if(count == 0){ 
  36.                         throw new CommonException("接口無訪問權限"); 
  37.                     } 
  38.                 } 
  39.             } 
  40.         } 
  41.     } 
  • 我們以【角色管理-查詢】為例,先新建一個請求實體類RoleDto,添加用戶ID屬性
  1. @Data 
  2. @EqualsAndHashCode(callSuper = false
  3. @Accessors(chain = true
  4. public class RoleDto extends Role { 
  5.  
  6.  //添加用戶ID 
  7.     private Long userId; 
  • 在需要的接口上,添加@CheckPermissions注解,增加權限控制
  1. @RestController 
  2. @RequestMapping("/role"
  3. public class RoleController { 
  4.  
  5.     private RoleService roleService; 
  6.  
  7.     @CheckPermissions(value="roleMgr:list"
  8.     @PostMapping(value = "/queryRole"
  9.     public List<Role> queryRole(RoleDto roleDto){ 
  10.         return roleService.list(); 
  11.     } 
  12.  
  13.     @CheckPermissions(value="roleMgr:add"
  14.     @PostMapping(value = "/addRole"
  15.     public void addRole(RoleDto roleDto){ 
  16.         roleService.add(roleDto); 
  17.     } 
  18.  
  19.     @CheckPermissions(value="roleMgr:delete"
  20.     @PostMapping(value = "/deleteRole"
  21.     public void deleteRole(RoleDto roleDto){ 
  22.         roleService.delete(roleDto); 
  23.     } 

依次類推,當我們想對某個接口進行權限控制的時候,只需要添加一個注解@CheckPermissions,并填寫對應的菜單編碼即可!

四、用戶權限

測試我們先初始化一個用戶【張三】,然后給他分配一個角色【訪客人員】,同時給這個角色分配一下2個菜單權限【系統配置】、【用戶管理】,等會用于權限測試。

初始內容如下:

 

數據初始化完成之后,我們來啟動項目,傳入用戶【張三】的ID,查詢用戶具備的菜單權限,結果如下:

 

 

查詢結果,用戶【張三】有兩個菜單權限!

接著,我們來驗證一下,用戶【張三】是否有角色查詢權限,請求角色查詢接口如下:

 

因為沒有配置角色查詢接口,所以無權訪問!

五、總結

整片內容,只介紹了后端關鍵的服務實現過程,可能也有遺漏的地方,歡迎網友點評、吐槽!

 

責任編輯:武曉燕 來源: Java極客技術
相關推薦

2018-03-23 20:45:23

機器學習NLP文本數據

2020-12-08 10:32:15

Python郵件tcp

2021-07-14 09:00:00

JavaFX開發應用

2011-01-10 14:41:26

2011-05-03 15:59:00

黑盒打印機

2025-05-07 00:31:30

2022-02-23 20:53:54

數據清洗模型

2020-12-07 09:01:58

冪等系統f(f(x)) =f(

2023-04-26 12:46:43

DockerSpringKubernetes

2022-01-08 20:04:20

攔截系統調用

2022-03-14 14:47:21

HarmonyOS操作系統鴻蒙

2022-07-27 08:16:22

搜索引擎Lucene

2022-12-07 08:42:35

2011-02-22 13:46:27

微軟SQL.NET

2021-02-26 11:54:38

MyBatis 插件接口

2021-12-28 08:38:26

Linux 中斷喚醒系統Linux 系統

2021-06-08 09:49:01

協程池Golang設計

2020-07-23 14:39:28

系統權限設計

2024-03-05 18:27:43

2024-04-02 08:58:13

點贊
收藏

51CTO技術棧公眾號

主站蜘蛛池模板: 日韩精品成人一区二区三区视频 | hitomi一区二区三区精品 | 欧美激情在线精品一区二区三区 | 一区二区视屏 | 亚洲国产一区二区视频 | h在线 | 欧美一级电影免费 | 国产福利免费视频 | 狠狠色狠狠色综合日日92 | 日韩三片| 国产色在线 | 国产精品美女一区二区 | 亚洲三区在线 | 日韩欧美网 | 欧美视频免费在线 | 久在草| 亚洲视频在线一区 | 免费一级黄色电影 | 亚洲欧美日韩中文字幕一区二区三区 | 91佛爷在线观看 | 国产高清免费视频 | 国产精品色哟哟网站 | 免费一级片 | 中文在线а√在线8 | 一区二区三区四区国产 | aa级毛片毛片免费观看久 | 欧美精品一区二区在线观看 | 欧美日韩一区在线 | av午夜电影 | www.97zyz.com| 国产精品激情 | 玖玖在线精品 | 一区二区三区国产精品 | 综合精品久久久 | www.久久久.com | 日韩欧美在线视频观看 | 日本在线免费看最新的电影 | 国产激情视频在线免费观看 | 亚洲免费大片 | 成人片免费看 | 一区精品国产欧美在线 |