松哥最近正在錄制 TienChin 項目視訊~采用 Spring Boot+Vue3 技術棧,裡邊會涉及到各種好玩的技術,小夥伴們來和松哥一起做一個完成率超 90% 的項目,戳戳戳這裡-->TienChin 項目配套視訊來啦
昨天的文章和小夥伴們簡單分享了權限細化到按鈕的思路,傳送門:
- 權限想要細化到按鈕,怎麼做?
這篇文章最後留下來一個問題,就是使用者的權限該如何設定?今天我們就來聊聊這個話題。
1. 角色與權限
首先我們先來看看角色與權限,該如何設計角色與權限,其實有很多非常成熟的理論,最為常見的莫過于 RBAC 了。
1.1 RBAC 簡介
RBAC(Role-based access control)是一種以角色為基礎的通路控制(Role-based access control,RBAC),它是一種較新且廣為使用的權限控制機制,這種機制不是直接給使用者賦予權限,而是将權限賦予角色。
RBAC 權限模型将使用者按角色進行歸類,通過使用者的角色來确定使用者對某項資源是否具備操作權限。RBAC 簡化了使用者與權限的管理,它将使用者與角色關聯、角色與權限關聯、權限與資源關聯,這種模式使得使用者的授權管理變得非常簡單和易于維護。
1.2 RBAC 的提出
權限、角色這些東西,在早期 1970 年代的商業計算機程式中就可以找到相關的應用,但是早期的程式相對簡單,而且并不存在一個明确的、通用的、公認的權限管理模型。
Ferraiolo 和 Kuhn 兩位大佬于 1992 年提出了一種基于通用角色的通路控制模型(看來這個模型比松哥年齡還大),首次提出了 RBAC 權限模型用來代替傳統的 MAC 和 DAC 兩種權限控制方案,并且就 RBAC 中的相關概念給出了解釋。
Ferraiolo,Cugini 和 Kuhn 于 1995 年擴充了 1992 年提出的權限模型。該模型的主要功能是所有通路都是通過角色進行的,而角色本質上是權限的集合,并且所有使用者隻能通過角色獲得權限。在組織内,角色相對穩定,而使用者和權限都很多,并且可能會迅速變化。是以,通過角色控制權限可以簡化通路控制的管理和檢查。
到了 1996 年,Sandhu,Coyne,Feinstein 和 Youman 正式提出了 RBAC 模型,該模型以子產品化方式細化了 RBAC,并提出了基于該理論的 RBAC0-RBAC3 四種不同模型。
今天,大多數資訊技術供應商已将 RBAC 納入其産品線,除了正常的企業級應用,RBAC 也廣泛應用在醫療、國防等領域。
目前網上關于 RBAC 理論性的東西松哥隻找到英文的,感興趣的小夥伴可以看下,位址是:
- https://csrc.nist.gov/projects/Role-Based-Access-Control
如果小夥伴們有中文的資料連結,歡迎留言說明。
1.3 RBAC 三原則
- 最小權限:給角色配置的權限是其完成任務所需要的最小權限集合。
- 責任分離:通過互相獨立互斥的角色來共同完成任務。
- 資料抽象:通過權限的抽象來展現,RBAC 支援的資料抽象程度與 RBAC 的實作細節有關。
1.4 RBAC 模型分類
說到 RBAC,我們就得從它的模型分類開始看起。
1.4.1 RBAC0
RBAC0 是最簡單的使用者、角色、權限模型。RBAC0 是 RBAC 權限模型中最核心的一部分,後面其他模型都是在此基礎上建立。

圖檔源自網絡
在 RBAC0 中,一個使用者可以具備多個角色,一個角色可以具備多個權限,最終使用者所具備的權限是使用者所具備的角色的權限并集。
1.4.2 RBAC1
RBAC1 則是在 RABC0 的基礎上引入了角色繼承,讓角色有了上下級關系。
圖檔源自網絡
在本系列前面的文章中,松哥也曾多次向大家介紹過 Spring Security 中的角色繼承。
1.4.3 RBAC2
RBAC2 也是在 RBAC0 的基礎上進行擴充,引入了靜态職責分離和動态職責分離。
圖檔源自網絡
要了解職責分離,我們得先明白角色互斥。
在實際項目中,有一些角色是互斥的,對立的,例如财務這個角色一般是不能和其他角色兼任的,否則自己報賬自己審批,豈不是爽歪歪!
通過職責分離可以解決這個問題:
靜态職責分離
在設定階段就做好了限制。比如同一使用者不能授予互斥的角色,使用者隻能有有限個角色,使用者獲得進階權限之前要有低級權限等等。
動态職責分離
在運作階段進行限制。比如運作時同一使用者下5個角色中隻能同時有2個角色激活等等。
1.4.4 RBAC3
将 RBAC1 和 RBAC2 結合起來,就形成了 RBAC3。
圖檔源自網絡
1.5 擴充
我們日常見到的很多權限模型都是在 RBAC 的基礎上擴充出來的。
例如在有的系統中我們可以見到使用者組的概念,就是将使用者分組,使用者同時具備自身的角色以及分組的角色。
我們 TienChin 項目所用的腳手架中的權限,就基本上是按照 RBAC 這套權限模型來的。
2. 表設計
我們來看下 RuoYi-Vue 腳手架中跟使用者、角色以及權限相關的表。
這裡主要涉及到如下幾張表:
-
:這個是使用者表。sys_user
-
:這個是角色表。sys_role
-
:這個是使用者角色關聯表。sys_user_role
-
:這個是菜單表,也可以了解為是資源表。sys_menu
-
:這個是資源角色關聯表。sys_role_menu
通過使用者的 id,可以去
sys_user_role
表中查詢到這個使用者具備的角色 id,再根據角色 id,去
sys_role_menu
表中查詢到這個角色可以操作的資源 id,再根據資源 id,去
sys_menu
表中查詢到對應的資源,基本上就是這個樣一個流程。
那麼 Java 代碼中該怎麼做呢?
3. 代碼實作
首先定義了一個 Java 類 SysUser,這個跟資料庫中的
sys_user
表是對應的,我們來看
UserDetailsService
的具體實作:
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
private static final Logger log = LoggerFactory.getLogger(UserDetailsServiceImpl.class);
@Autowired
private ISysUserService userService;
@Autowired
private SysPermissionService permissionService;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
SysUser user = userService.selectUserByUserName(username);
if (StringUtils.isNull(user)) {
log.info("登入使用者:{} 不存在.", username);
throw new ServiceException("登入使用者:" + username + " 不存在");
} else if (UserStatus.DELETED.getCode().equals(user.getDelFlag())) {
log.info("登入使用者:{} 已被删除.", username);
throw new ServiceException("對不起,您的賬号:" + username + " 已被删除");
} else if (UserStatus.DISABLE.getCode().equals(user.getStatus())) {
log.info("登入使用者:{} 已被停用.", username);
throw new ServiceException("對不起,您的賬号:" + username + " 已停用");
}
return createLoginUser(user);
}
public UserDetails createLoginUser(SysUser user) {
return new LoginUser(user.getUserId(), user.getDeptId(), user, permissionService.getMenuPermission(user));
}
}
複制
從資料庫中查詢到的就是 SysUser 對象,然後對該對象稍作改造,将之改造成為一個 LoginUser 對象,這個 LoginUser 則是 UserDetails 接口的實作類,裡邊儲存了目前登入使用者的關鍵資訊。
在建立 LoginUser 對象的時候,有一個 permissionService.getMenuPermission 方法用來查詢使用者的權限,根據目前使用者的 id,查詢到使用者的角色,再根據使用者角色,查詢到使用者的權限,另外,如果目前使用者的角色是 admin,那麼就設定使用者角色為
*:*:*
,這是一段寫死。
我們再來看看 LoginUser 的設計:
public class LoginUser implements UserDetails {
/**
* 權限清單
*/
private Set<String> permissions;
/**
* 使用者資訊
*/
private SysUser user;
public LoginUser(Long userId, Long deptId, SysUser user, Set<String> permissions) {
this.userId = userId;
this.deptId = deptId;
this.user = user;
this.permissions = permissions;
}
@JSONField(serialize = false)
@Override
public String getPassword() {
return user.getPassword();
}
@Override
public String getUsername() {
return user.getUserName();
}
/**
* 賬戶是否未過期,過期無法驗證
*/
@JSONField(serialize = false)
@Override
public boolean isAccountNonExpired() {
return true;
}
/**
* 指定使用者是否解鎖,鎖定的使用者無法進行身份驗證
*
* @return
*/
@JSONField(serialize = false)
@Override
public boolean isAccountNonLocked() {
return true;
}
/**
* 訓示是否已過期的使用者的憑據(密碼),過期的憑據防止認證
*
* @return
*/
@JSONField(serialize = false)
@Override
public boolean isCredentialsNonExpired() {
return true;
}
/**
* 是否可用 ,禁用的使用者不能身份驗證
*
* @return
*/
@JSONField(serialize = false)
@Override
public boolean isEnabled() {
return true;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return null;
}
}
複制
有一些屬性我省略掉了,大家可以文末下載下傳源碼檢視。
小夥伴們看到,這個 LoginUser 實作了 UserDetails 接口,但是和 vhr 中有一個很大的不同,就是這裡沒有處理 getAuthorities 方法,也就是說當系統想要去擷取使用者權限的時候,二話不說直接傳回一個 null。這是咋回事呢?
因為在這個腳手架中,将來進行權限校驗的時候,是按照下面這樣來的:
@PreAuthorize("@ss.hasPermi('system:menu:add')")
@PostMapping
public AjaxResult add(@Validated @RequestBody SysMenu menu) {
//省略
}
複制
@PreAuthorize 注解中的
@ss.hasPermi('system:menu:add')
表達式,表示調用 Spring 容器中一個名為 ss 的 Bean 的 hasPermi 方法,去判斷目前使用者是否具備一個名為
system:menu:add
的權限。一個名為 ss 的 Bean 的 hasPermi 方法如下:
@Service("ss")
public class PermissionService {
/**
* 所有權限辨別
*/
private static final String ALL_PERMISSION = "*:*:*";
/**
* 管理者角色權限辨別
*/
private static final String SUPER_ADMIN = "admin";
private static final String ROLE_DELIMETER = ",";
private static final String PERMISSION_DELIMETER = ",";
/**
* 驗證使用者是否具備某權限
*
* @param permission 權限字元串
* @return 使用者是否具備某權限
*/
public boolean hasPermi(String permission) {
if (StringUtils.isEmpty(permission)) {
return false;
}
LoginUser loginUser = SecurityUtils.getLoginUser();
if (StringUtils.isNull(loginUser) || CollectionUtils.isEmpty(loginUser.getPermissions())) {
return false;
}
return hasPermissions(loginUser.getPermissions(), permission);
}
/**
* 判斷是否包含權限
*
* @param permissions 權限清單
* @param permission 權限字元串
* @return 使用者是否具備某權限
*/
private boolean hasPermissions(Set<String> permissions, String permission) {
return permissions.contains(ALL_PERMISSION) || permissions.contains(StringUtils.trim(permission));
}
}
複制
由于這裡是純手工操作,在比較的時候,直接擷取到目前登入使用者對象 LoginUser,再手動調用他的 hasPermissions 方法去判斷權限是否滿足,由于都是自定義操作,是以是否實作 UserDetails#getAuthorities 方法已經不重要了,不過按照這裡的比對方案,是不支援通配符的比對的。
例如使用者具備針對字典表的所有操作權限,表示為
system:dict:*
,但是當和
system:dict:list
進行比較的時候,發現比較結果為 false,這塊想要比對成功也是可以的,例如可以通過正規表達式或者其他方式來操作,反正都是字元串比較,相信大家都能自己搞得定。
現在,前端提供操作頁面,也可以配置每一個使用者的角色,也可以配置每一個角色可以操作的權限就行了,這個就比較簡單了,不多說。
好啦,這就是 TienChin 項目中的 RBAC 權限實作方案,後面松哥也會錄制相關的視訊教程,對視訊感興趣的小夥伴戳這裡:TienChin 項目配套視訊來啦。