Spring Security 中如何讓上級擁有下級的所有權限(案例分析)
答案是能!
松哥之前寫過類似的文章,但是主要是講了用法,今天我們來看看原理!
本文基于當前 Spring Security 5.3.4 來分析,為什么要強調最新版呢?因為在在 5.0.11 版中,角色繼承配置和現在不一樣。舊版的方案我們現在不討論了,直接來看當前最新版是怎么處理的。
1.角色繼承案例
我們先來一個簡單的權限案例。
創(chuàng)建一個 Spring Boot 項目,添加 Spring Security 依賴,并創(chuàng)建兩個測試用戶,如下:
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser("javaboy")
.password("{noop}123").roles("admin")
.and()
.withUser("江南一點雨")
.password("{noop}123")
.roles("user");
}
然后準備三個測試接口,如下:
@RestController
public class HelloController {
@GetMapping("/hello")
public String hello() {
return "hello";
}
@GetMapping("/admin/hello")
public String admin() {
return "admin";
}
@GetMapping("/user/hello")
public String user() {
return "user";
}
}
這三個測試接口,我們的規(guī)劃是這樣的:
- /hello 是任何人都可以訪問的接口
- /admin/hello 是具有 admin 身份的人才能訪問的接口
- /user/hello 是具有 user 身份的人才能訪問的接口
- 所有 user 能夠訪問的資源,admin 都能夠訪問
注意第四條規(guī)范意味著所有具備 admin 身份的人自動具備 user 身份。
接下來我們來配置權限的攔截規(guī)則,在 Spring Security 的 configure(HttpSecurity http) 方法中,代碼如下:
http.authorizeRequests()
.antMatchers("/admin/**").hasRole("admin")
.antMatchers("/user/**").hasRole("user")
.anyRequest().authenticated()
.and()
...
...
這里的匹配規(guī)則我們采用了 Ant 風格的路徑匹配符,Ant 風格的路徑匹配符在 Spring 家族中使用非常廣泛,它的匹配規(guī)則也非常簡單:
| 通配符 | 含義 |
|---|---|
| ** | 匹配多層路徑 |
| * | 匹配一層路徑 |
| ? | 匹配任意單個字符 |
上面配置的含義是:
- 如果請求路徑滿足
/admin/**格式,則用戶需要具備 admin 角色。 - 如果請求路徑滿足
/user/**格式,則用戶需要具備 user 角色。 - 剩余的其他格式的請求路徑,只需要認證(登錄)后就可以訪問。
注意代碼中配置的三條規(guī)則的順序非常重要,和 Shiro 類似,Spring Security 在匹配的時候也是按照從上往下的順序來匹配,一旦匹配到了就不繼續(xù)匹配了,所以攔截規(guī)則的順序不能寫錯。
如果使用角色繼承,這個功能很好實現,我們只需要在 SecurityConfig 中添加如下代碼來配置角色繼承關系即可:
@Bean
RoleHierarchy roleHierarchy() {
RoleHierarchyImpl hierarchy = new RoleHierarchyImpl();
hierarchy.setHierarchy("ROLE_admin > ROLE_user");
return hierarchy;
}
注意,在配置時,需要給角色手動加上 ROLE_ 前綴。上面的配置表示 ROLE_admin 自動具備 ROLE_user 的權限。
接下來,我們啟動項目進行測試。
項目啟動成功后,我們首先以 江南一點雨的身份進行登錄:

登錄成功后,分別訪問 /hello,/admin/hello 以及 /user/hello 三個接口,其中:
/hello因為登錄后就可以訪問,這個接口訪問成功。/admin/hello需要 admin 身份,所以訪問失敗。/user/hello需要 user 身份,所以訪問成功。
再以 javaboy 身份登錄,登錄成功后,我們發(fā)現 javaboy 也能訪問 /user/hello 這個接口了,說明我們的角色繼承配置沒問題!
2.原理分析
這里配置的核心在于我們提供了一個 RoleHierarchy 實例,所以我們的分析就從該類入手。
RoleHierarchy 是一個接口,該接口中只有一個方法:
public interface RoleHierarchy {
Collection<? extends GrantedAuthority> getReachableGrantedAuthorities(
Collection<? extends GrantedAuthority> authorities);
}
這個方法參數 authorities 是一個權限集合,從方法名上看方法的返回值是一個可訪問的權限集合。
舉個簡單的例子,假設角色層次結構是 ROLE_A > ROLE_B > ROLE_C,現在直接給用戶分配的權限是 ROLE_A,但實際上用戶擁有的權限有 ROLE_A、ROLE_B 以及 ROLE_C。
getReachableGrantedAuthorities 方法的目的就是是根據角色層次定義,將用戶真正可以觸達的角色解析出來。
RoleHierarchy 接口有兩個實現類,如下圖:

- NullRoleHierarchy 這是一個空的實現,將傳入的參數原封不動返回。
- RoleHierarchyImpl 這是我們上文所使用的實現,這個會完成一些解析操作。
我們來重點看下 RoleHierarchyImpl 類。
這個類中實際上就四個方法 setHierarchy、getReachableGrantedAuthorities、buildRolesReachableInOneStepMap 以及 buildRolesReachableInOneOrMoreStepsMap,我們來逐個進行分析。
首先是我們一開始調用的 setHierarchy 方法,這個方法用來設置角色層級關系:
public void setHierarchy(String roleHierarchyStringRepresentation) {
this.roleHierarchyStringRepresentation = roleHierarchyStringRepresentation;
if (logger.isDebugEnabled()) {
logger.debug("setHierarchy() - The following role hierarchy was set: "
+ roleHierarchyStringRepresentation);
}
buildRolesReachableInOneStepMap();
buildRolesReachableInOneOrMoreStepsMap();
}
用戶傳入的字符串變量設置給 roleHierarchyStringRepresentation 屬性,然后通過 buildRolesReachableInOneStepMap 和 buildRolesReachableInOneOrMoreStepsMap 方法完成對角色層級的解析。
buildRolesReachableInOneStepMap 方法用來將角色關系解析成一層一層的形式。我們來看下它的源碼:
private void buildRolesReachableInOneStepMap() {
this.rolesReachableInOneStepMap = new HashMap<>();
for (String line : this.roleHierarchyStringRepresentation.split("\n")) {
String[] roles = line.trim().split("\\s+>\\s+");
for (int i = 1; i < roles.length; i++) {
String higherRole = roles[i - 1];
GrantedAuthority lowerRole = new SimpleGrantedAuthority(roles[i]);
Set<GrantedAuthority> rolesReachableInOneStepSet;
if (!this.rolesReachableInOneStepMap.containsKey(higherRole)) {
rolesReachableInOneStepSet = new HashSet<>();
this.rolesReachableInOneStepMap.put(higherRole, rolesReachableInOneStepSet);
} else {
rolesReachableInOneStepSet = this.rolesReachableInOneStepMap.get(higherRole);
}
rolesReachableInOneStepSet.add(lowerRole);
}
}
}
首先大家看到,按照換行符來解析用戶配置的多個角色層級,這是什么意思呢?
我們前面案例中只是配置了 ROLE_admin > ROLE_user,如果你需要配置多個繼承關系,怎么配置呢?多個繼承關系用 \n 隔開即可,如下 ROLE_A > ROLE_B \n ROLE_C > ROLE_D。還有一種情況,如果角色層級關系是連續(xù)的,也可以這樣配置 ROLE_A > ROLE_B > ROLE_C > ROLE_D。
所以這里先用 \n 將多層繼承關系拆分開形成一個數組,然后對數組進行遍歷。
在具體遍歷中,通過 > 將角色關系拆分成一個數組,然后對數組進行解析,高一級的角色作為 key,低一級的角色作為 value。
代碼比較簡單,最終的解析出來存入 rolesReachableInOneStepMap 中的層級關系是這樣的:
假設角色繼承關系是 ROLE_A > ROLE_B \n ROLE_C > ROLE_D \n ROLE_C > ROLE_E,Map 中的數據是這樣:
- A–>B
- C–>[D,E]
假設角色繼承關系是 ROLE_A > ROLE_B > ROLE_C > ROLE_D,Map 中的數據是這樣:
- A–>B
- B–>C
- C–>D
這是 buildRolesReachableInOneStepMap 方法解析出來的 rolesReachableInOneStepMap 集合。
接下來的 buildRolesReachableInOneOrMoreStepsMap 方法則是對 rolesReachableInOneStepMap 集合進行再次解析,將角色的繼承關系拉平。
例如 rolesReachableInOneStepMap 中保存的角色繼承關系如下:
- A–>B
- B–>C
- C–>D
經過 buildRolesReachableInOneOrMoreStepsMap 方法解析之后,新的 Map 中保存的數據如下:
- A–>[B、C、D]
- B–>[C、D]
- C–>D
這樣解析完成后,每一個角色可以觸達到的角色就一目了然了。
我們來看下 buildRolesReachableInOneOrMoreStepsMap 方法的實現邏輯:
private void buildRolesReachableInOneOrMoreStepsMap() {
this.rolesReachableInOneOrMoreStepsMap = new HashMap<>();
for (String roleName : this.rolesReachableInOneStepMap.keySet()) {
Set<GrantedAuthority> rolesToVisitSet = new HashSet<>(this.rolesReachableInOneStepMap.get(roleName));
Set<GrantedAuthority> visitedRolesSet = new HashSet<>();
while (!rolesToVisitSet.isEmpty()) {
GrantedAuthority lowerRole = rolesToVisitSet.iterator().next();
rolesToVisitSet.remove(lowerRole);
if (!visitedRolesSet.add(lowerRole) ||
!this.rolesReachableInOneStepMap.containsKey(lowerRole.getAuthority())) {
continue;
} else if (roleName.equals(lowerRole.getAuthority())) {
throw new CycleInRoleHierarchyException();
}
rolesToVisitSet.addAll(this.rolesReachableInOneStepMap.get(lowerRole.getAuthority()));
}
this.rolesReachableInOneOrMoreStepsMap.put(roleName, visitedRolesSet);
}
}
這個方法還比較巧妙。首先根據 roleName 從 rolesReachableInOneStepMap 中獲取對應的 rolesToVisitSet,這個 rolesToVisitSet 是一個 Set 集合,對其進行遍歷,將遍歷結果添加到 visitedRolesSet 集合中,如果 rolesReachableInOneStepMap 集合的 key 不包含當前讀取出來的 lowerRole,說明這個 lowerRole 就是整個角色體系中的最底層,直接 continue。否則就把 lowerRole 在 rolesReachableInOneStepMap 中對應的 value 拿出來繼續(xù)遍歷。
最后將遍歷結果存入 rolesReachableInOneOrMoreStepsMap 集合中即可。
這個方法有點繞,小伙伴們可以自己打個斷點品一下。
看了上面的分析,小伙伴們可能發(fā)現了,其實角色繼承,最終還是拉平了去對比。
我們定義的角色有層級,但是代碼中又將這種層級拉平了,方便后續(xù)的比對。
最后還有一個 getReachableGrantedAuthorities 方法,根據傳入的角色分析出其可能潛在包含的一些角色:
public Collection<GrantedAuthority> getReachableGrantedAuthorities(
Collection<? extends GrantedAuthority> authorities) {
if (authorities == null || authorities.isEmpty()) {
return AuthorityUtils.NO_AUTHORITIES;
}
Set<GrantedAuthority> reachableRoles = new HashSet<>();
Set<String> processedNames = new HashSet<>();
for (GrantedAuthority authority : authorities) {
if (authority.getAuthority() == null) {
reachableRoles.add(authority);
continue;
}
if (!processedNames.add(authority.getAuthority())) {
continue;
}
reachableRoles.add(authority);
Set<GrantedAuthority> lowerRoles = this.rolesReachableInOneOrMoreStepsMap.get(authority.getAuthority());
if (lowerRoles == null) {
continue;
}
for (GrantedAuthority role : lowerRoles) {
if (processedNames.add(role.getAuthority())) {
reachableRoles.add(role);
}
}
}
List<GrantedAuthority> reachableRoleList = new ArrayList<>(reachableRoles.size());
reachableRoleList.addAll(reachableRoles);
return reachableRoleList;
}
這個方法的邏輯比較直白,就是從 rolesReachableInOneOrMoreStepsMap 集合中查詢出當前角色真正可訪問的角色信息。
3.RoleHierarchyVoter
getReachableGrantedAuthorities 方法將在 RoleHierarchyVoter 投票器中被調用。
public class RoleHierarchyVoter extends RoleVoter {
private RoleHierarchy roleHierarchy = null;
public RoleHierarchyVoter(RoleHierarchy roleHierarchy) {
Assert.notNull(roleHierarchy, "RoleHierarchy must not be null");
this.roleHierarchy = roleHierarchy;
}
@Override
Collection<? extends GrantedAuthority> extractAuthorities(
Authentication authentication) {
return roleHierarchy.getReachableGrantedAuthorities(authentication
.getAuthorities());
}
}
關于 Spring Security 投票器,將是另外一個故事,松哥將在下篇文章中和小伙伴們分享投票器和決策器~
4.小結
到此這篇關于Spring Security 中如何讓上級擁有下級的所有權限的文章就介紹到這了,更多相關Spring Security上級擁有下級的所有權限內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
- SpringBoot2.0 整合 SpringSecurity 框架實現用戶權限安全管理方法
- SpringBoot+Spring Security+JWT實現RESTful Api權限控制的方法
- spring security動態(tài)配置url權限的2種實現方法
- SpringBoot+Vue前后端分離,使用SpringSecurity完美處理權限問題的解決方法
- Spring Boot中使用 Spring Security 構建權限系統(tǒng)的示例代碼
- 話說Spring Security權限管理(源碼詳解)
- java中自定義Spring Security權限控制管理示例(實戰(zhàn)篇)
- Spring security實現權限管理示例
相關文章
spring中使用mybatis plus連接sqlserver的方法實現
這篇文章主要介紹了spring中使用mybatis plus連接sqlserver的方法實現,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2020-12-12
springboot aspect通過@annotation進行攔截的實例代碼詳解
這篇文章主要介紹了springboot aspect通過@annotation進行攔截的方法,本文通過實例代碼給大家介紹的非常詳細,對大家的學習或工作具有一定的參考借鑒價值,需要的朋友可以參考下2020-08-08
SpringBoot使用JavaMailSender實現發(fā)送郵件+Excel附件
項目審批完畢后,需要發(fā)送郵件通知相關人員,并且要附帶數據庫表生成的Excel表格,這就要求不光是郵件發(fā)送功能,還要臨時生成Excel表格做為附件,本文詳細介紹了SpringBoot如何使用JavaMailSender實現發(fā)送郵件+Excel附件,需要的朋友可以參考下2023-10-10

