權(quán)限想要細化到按鈕,怎么做?
因為寫了不少 Spring Security 文章的緣故,所以總是有小伙伴來問松哥:按鈕級別的權(quán)限怎么實現(xiàn)?甚至有一些看過 vhr 的小伙伴也問這種問題,其實有的時候搞得我確實挺郁悶的,最近剛好要做 TienChin 項目,我就再把這個問題拎出來和小伙伴們仔細捋一捋。
1. 權(quán)限顆粒度
首先小伙伴們都知道權(quán)限有不同的顆粒度,在 vhr 項目中,整體上我是基于請求地址去處理權(quán)限的,這個粒度算粗還是算細呢?
有的小伙伴們可能認為這個權(quán)限粒度太粗,所謂細粒度的權(quán)限應該是基于按鈕的。
如果有小伙伴們做過前后端不分的開發(fā),應該會有這樣的體會:在 Shiro 或者 Spring Security 框架中,都提供了一些標簽,通過這些標簽可以做到在滿足某種角色或者權(quán)限的情況下,顯示某個按鈕;當用戶不具備某種角色或者權(quán)限的時候,按鈕則會自動隱藏起來。
但是大家想想,按鈕的顯示與隱藏不過是前端頁面為了提高用戶體驗而作出的樣式的變化而已,本質(zhì)上,當你點擊一個按鈕的時候,還是發(fā)送了一個 HTTP 請求,那么服務端處理該請求的接口,必須要進行權(quán)限控制。既然要在接口上進行權(quán)限控制,那么跟 vhr 的區(qū)別在哪里呢?
現(xiàn)在流行前后端分離開發(fā),所以 Shiro 或者 Spring Security 中的那些前端標簽現(xiàn)在基本上都不用了,取而代之的做法是用戶在登錄成功之后,向服務端發(fā)送請求,獲取當前登錄用戶的權(quán)限以及角色信息,然后根據(jù)這些權(quán)限、角色等信息,在前端自動的去判斷一個菜單或者按鈕應該是顯示還是隱藏,這么做的目的是為了提高用戶體驗,避免用戶點擊一個沒有權(quán)限的按鈕。前端的顯示或者隱藏僅僅只是為了提高用戶體驗,真正的權(quán)限控制還是要后端來做。
后端可以在接口或者業(yè)務層對權(quán)限進行處理,具體在哪里做,就要看各自的項目了。
所以,vhr 中的權(quán)限,從設計上來說,粒度并不算粗,也是細粒度的,只不過跟菜單表放在了一起,小伙伴們可能感覺有點粗。但是,菜單表是可以繼續(xù)細化的,我們可以繼續(xù)在菜單表中添加新的記錄,新記錄的 hidden 字段為 true,則菜單是隱藏的,就單純只是細化權(quán)限而已。
如下圖可以繼續(xù)添加新的訪問規(guī)則,只不過把 enabled 字段設置為 false 即可(這樣菜單就不會顯示出來了,單純就只是權(quán)限的配置)。
所以 vhr 的權(quán)限設計是 OK 的。
當你理解了 vhr 中的權(quán)限設計,再來看 TienChin 這個項目,或者說看 RuoYi-Vue 這個腳手架,就會發(fā)現(xiàn)非常 easy 了。
2. 權(quán)限表
首先我們來看看資源表的定義,也就是 sys_menu。
CREATE TABLE `sys_menu` (
`menu_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '菜單ID',
`menu_name` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '菜單名稱',
`parent_id` bigint(20) DEFAULT '0' COMMENT '父菜單ID',
`order_num` int(4) DEFAULT '0' COMMENT '顯示順序',
`path` varchar(200) COLLATE utf8mb4_unicode_ci DEFAULT '' COMMENT '路由地址',
`component` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '組件路徑',
`query` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '路由參數(shù)',
`is_frame` int(1) DEFAULT '1' COMMENT '是否為外鏈(0是 1否)',
`is_cache` int(1) DEFAULT '0' COMMENT '是否緩存(0緩存 1不緩存)',
`menu_type` char(1) COLLATE utf8mb4_unicode_ci DEFAULT '' COMMENT '菜單類型(M目錄 C菜單 F按鈕)',
`visible` char(1) COLLATE utf8mb4_unicode_ci DEFAULT '0' COMMENT '菜單狀態(tài)(0顯示 1隱藏)',
`status` char(1) COLLATE utf8mb4_unicode_ci DEFAULT '0' COMMENT '菜單狀態(tài)(0正常 1停用)',
`perms` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '權(quán)限標識',
`icon` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT '#' COMMENT '菜單圖標',
`create_by` varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT '' COMMENT '創(chuàng)建者',
`create_time` datetime DEFAULT NULL COMMENT '創(chuàng)建時間',
`update_by` varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT '' COMMENT '更新者',
`update_time` datetime DEFAULT NULL COMMENT '更新時間',
`remark` varchar(500) COLLATE utf8mb4_unicode_ci DEFAULT '' COMMENT '備注',
PRIMARY KEY (`menu_id`)
) ENGINE=InnoDB AUTO_INCREMENT=3054 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='菜單權(quán)限表';
其實這里很多字段都和我們 vhr 項目項目很相似,我也就不重復啰嗦了,我這里主要和小伙伴們說一個字段,那就是 menu_type。
menu_type 表示一個菜單字段的類型,一個菜單有三種類型,分別是目錄(M)、菜單(C)以及按鈕(F)。這里所說的目錄,相當于我們在 vhr 中所說的一級菜單,菜單相當于我們在 vhr 中所說的二級菜單。
當用戶從前端登錄成功后,要去動態(tài)加載的菜單的時候,就查詢 M 和 C 類型的數(shù)據(jù)即可,F(xiàn) 類型的數(shù)據(jù)不是菜單項,查詢的時候直接過濾掉即可,通過 menu_type 這個字段可以輕松的過濾掉 F 類型的數(shù)據(jù)。小伙伴們想想,F(xiàn) 類型的數(shù)據(jù)過濾掉之后,剩下的數(shù)據(jù)不就是一級菜單和二級菜單了,那不就和 vhr 又一樣了么!
最后再來說說 F 類型的,F(xiàn) 類型的就是按鈕級別的權(quán)限了,前端每一個按鈕的執(zhí)行,需要哪些權(quán)限,現(xiàn)在就在這里定義好。
舉一個簡單的例子大家來看下:
當需要展示用戶管理這個菜單的時候,需要 system:user:list? 這個權(quán)限,當需要點擊用戶修改這個按鈕的時候,則需要 system:user:edit 這個權(quán)限。
其他相關(guān)的表基本上和 vhr 都是一樣的,用戶有用戶表 sys_user?,角色有角色表 sys_role?,用戶和角色關(guān)聯(lián)的表是 sys_user_role?,資源和角色關(guān)聯(lián)的表是 sys_role_menu。
當用戶登錄成功后,后端會提供一個接口,將當前用戶的角色和權(quán)限統(tǒng)統(tǒng)返回給前端:
查詢角色思路:根據(jù)用戶 id,先去sys_user_role? 表中查詢到角色 id,再根據(jù)角色 id 去sys_role 表中查詢到對應的角色(這里為了方便大家理解這么描述,實際上一個多表聯(lián)合查詢即可)。
查詢權(quán)限思路:根據(jù)用戶 id,先去sys_user_role? 表中查詢到角色 id,再根據(jù)角色 id 去sys_role? 表中查詢到對應的角色,再拿著角色 id 去sys_role_menu? 表中查詢到對應的menu_id?,再根據(jù)menu_id? 去sys_menu 表中查詢到對應的 menu 中的權(quán)限(這里為了方便大家理解這么描述,實際上一個多表聯(lián)合查詢即可)。
前端有了用戶的權(quán)限以及角色之后,就可以自行決定是否顯示某一個菜單或者是否展示某一個按鈕了。
3. 后端權(quán)限判斷
我先來說說這塊 TienChin 項目中是怎么做的(即 RuoYi 腳手架的實現(xiàn)方案),再來和 vhr 進行一個對比。
在 TienChin 項目中是通過注解來控制權(quán)限的,接口的訪問權(quán)限都是通過注解來標記的,例如下面這種:
@PreAuthorize("@ss.hasPermi('system:menu:add')")
@PostMapping
public AjaxResult add(@Validated @RequestBody SysMenu menu) {
//省略
}
/**
* 修改菜單
*/
@PreAuthorize("@ss.hasPermi('system:menu:edit')")
@PutMapping
public AjaxResult edit(@Validated @RequestBody SysMenu menu) {
//省略
}
/**
* 刪除菜單
*/
@PreAuthorize("@ss.hasPermi('system:menu:remove')")
@DeleteMapping("/{menuId}")
public AjaxResult remove(@PathVariable("menuId") Long menuId) {
//省略
}
每一個接口需要什么權(quán)限,都是通過 @PreAuthorize 注解來實現(xiàn)的。
不過上面這種寫法說到底還是有一點“硬編碼”,因為訪問哪個接口需要哪些權(quán)限,在代碼中固定了,如果接口和權(quán)限直接的關(guān)系能夠保存到數(shù)據(jù)庫中,那么用戶就可以在自己需要的時候,隨時進行靈活修改,豈不美哉!
在 vhr 項目中,松哥利用 Spring Security 中自定義 FilterInvocationSecurityMetadataSource 和 AccessDecisionManager 實現(xiàn)了服務端動態(tài)控制權(quán)限。
相對來說,vhr 中的實現(xiàn)方案更靈活一些,因為可以配置接口和權(quán)限之間的關(guān)系。不過怎么說呢?其實像 RuoYi-Vue 這樣硬編碼其實也不是不可以,畢竟接口和權(quán)限之間的映射關(guān)系還是稍顯“專業(yè)”一些,普通用戶可能并不懂該如何配置,這個加入說系統(tǒng)提供了這個功能,那么更多的還是面向程序員這一類專業(yè)人員的,那么程序員到底是否需要這個功能呢?我覺得還是得具體情況具體分析。
總之,小伙伴們可以結(jié)合自己項目的實際情況,來決定接口和權(quán)限之間的映射關(guān)系是否需要動態(tài)管理,如果需要動態(tài)管理,那么可以按照 vhr 中的方案來,如果不需要動態(tài)管理,那么就按照 RuoYi-Vue 腳手架中的方式來就行了。
好啦,這就是 RuoYi-Vue 這個腳手架中關(guān)于權(quán)限的設計,現(xiàn)在有一個新的問題擺在面前:如何給用戶設置權(quán)限的?現(xiàn)在整個系統(tǒng)的權(quán)限架構(gòu)師安排的明明白白的,那么用戶的權(quán)限又是從何而來的呢?這個我們下篇文章繼續(xù)拆解。