在實際的專案開發過程中,選單許可權功能可以說是後端管理系統中必不可少的一個環節,根據業務的複雜度,設計的時候可深可淺,但無論怎麼變化,設計的思路基本都是圍繞著使用者、角色、選單進行相應的擴充套件。
今天小編就和大家一起來討論一下,怎麼設計一套可以精確到按鈕級別的選單許可權功能,廢話不多說,直接開擼!
先來看一下,使用者、角色、選單表對應的ER圖,如下:
其中,使用者和角色是多對多的關係,角色與選單也是多對多的關係,使用者通過角色來關聯到選單,當然也有的業務系統選單許可權模型,是可以直接通過使用者關聯到選單,對選單許可權可以直接控制到使用者級別,不過這個都不是問題,這個也可以進行擴充套件。
對於使用者、角色表比較簡單,下面,我們重點來看看選單表的設計,如下:
可以看到,整個選單表就是一個樹型結構,關鍵欄位說明:
為了後面方便開發,我們先建立一個名為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 NULL COMMENT '訊息給過來的ID',
mobile varchar(20) NOT NULL DEFAULT '' COMMENT '手機號',
name varchar(100) NOT NULL DEFAULT '' COMMENT '姓名',
password varchar(128) NOT NULL DEFAULT '' COMMENT '密碼',
is_delete tinyint(4) NOT NULL 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 NULL COMMENT '主鍵',
user_id bigint(20) NOT NULL COMMENT '使用者ID',
role_id bigint(20) NOT NULL 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 NULL COMMENT '主鍵',
code varchar(100) NOT NULL DEFAULT '' COMMENT '編碼',
name varchar(100) NOT NULL DEFAULT '' COMMENT '名稱',
is_delete tinyint(4) NOT NULL 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 NULL COMMENT '主鍵',
role_id bigint(20) NOT NULL COMMENT '角色ID',
menu_id bigint(20) NOT NULL 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 NULL COMMENT '主鍵',
name varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '名稱',
menu_code varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '選單編碼',
parent_id bigint(20) DEFAULT NULL COMMENT '父節點',
node_type tinyint(4) NOT NULL DEFAULT '1' COMMENT '節點型別,1資料夾,2頁面,3按鈕',
icon_url varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT '' COMMENT '圖示地址',
sort int(11) NOT NULL DEFAULT '1' COMMENT '排序號',
link_url varchar(500) COLLATE utf8mb4_unicode_ci DEFAULT '' COMMENT '頁面對應的地址',
level int(11) NOT NULL DEFAULT '0' COMMENT '層次',
path varchar(2500) COLLATE utf8mb4_unicode_ci DEFAULT '' COMMENT '樹id的路徑 整個層次上的路徑id,逗號分隔,想要找父節點特別快',
is_delete tinyint(4) NOT NULL 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張表就可以搞定,真正有點複雜的地方在於資料的寫入和渲染,當然如果老闆突然讓你來開發一套選單許可權系統,我們也沒必要慌張,下面,我們一起來看看後端應該如何開發。
為了方便快捷,小編我採用的是springboot+mybatisPlus
元件來快速開發,直接利用mybatisPlus
官方提供的快速生成程式碼的demo
,一鍵生成所需的dao
、service
、web
層的程式碼,結果如下:
@Override
public void addMenu(Menu menu) {
//如果插入的當前節點為根節點,parentId指定為0
if(menu.getParentId().longValue() == 0){
menu.setLevel(1);//根節點層級為1
menu.setPath(null);//根節點路徑為空
}else{
Menu parentMenu = baseMapper.selectById(menu.getParentId());
if(parentMenu == null){
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_id
、level
、path
,這三個欄位的寫入,如果新建的是根節點,預設parent_id
為0
,方便後續遞迴遍歷。
@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條資料,如下圖:
其中最後三條是按鈕型別,等下會用於後端許可權控制,介面查詢結果如下:
這個服務是針對後端管理介面查詢的,會將所有的選單全部查詢出來以便於進行管理,展示結果類似如下圖:
這個圖片截圖於小編正在開發的一個專案,內容可能不一致,但是資料結構基本都是一致的。
在上面,我們介紹到了使用者通過角色來關聯選單,因此,很容易想到,流程如下:
實現過程相比選單查詢服務多了前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 null;
}
@PostMapping(value = "/queryMenus")
public List<MenuVo> queryMenus(Long userId){
//查詢當前使用者下的選單許可權
return menuService.queryMenus(userId);
}
有的同學,可能覺得沒必要存放path
這個欄位,的確在某些場景下不需要。
為什麼要存放這個欄位呢?
小編在跟前端進行對接的時候,發現這麼一個問題,有些前端的樹型元件,在勾選子集的時候,不會將對應的父ID傳給後端,例如,我在勾選【列表查詢】的時候,前端無法將父節點【選單管理】ID也傳給後端,所有後端實際存放的是一個尾節點,需要一個欄位path
,來存放節點對應的父節點路徑。
其實,前端也可以傳,只不過需要修改元件的屬性,前端修改完成之後,樹型元件就無法全選,不滿足業務需求。
所以,有些時候得根據實際得情況來進行取捨。
後端進行許可權控制目標,主要是為了防止無許可權的使用者,進行介面請求查詢。
其中選單編碼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 = null;
Object[] args = joinPoint.getArgs();
Object parobj = args[0];
//使用者請求引數實體類中的使用者ID
if(!Objects.isNull(parobj)){
Class userCla = parobj.getClass();
Field field = userCla.getDeclaredField("userId");
field.setAccessible(true);
userId = (Long) field.get(parobj);
}
if(!Objects.isNull(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) != null){
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,查詢使用者具備的選單許可權,結果如下:
查詢結果,使用者【張三】有兩個選單許可權!
接著,我們來驗證一下,使用者【張三】是否有角色查詢許可權,請求角色查詢介面如下:
因為沒有設定角色查詢介面,所以無權存取!
整片內容,只介紹了後端關鍵的服務實現過程,可能也有遺漏的地方,歡迎網友點評、吐槽!
作者:程式設計師志哥
出處:www.pzblog.cn
資源:微信搜【Java極客技術】關注我,回覆 【cccc】有我準備的一執行緒序必備計算機書籍、大廠面試資料和免費電子書。 希望可以幫助大家提升技術和能力。