Spring Security 中如何讓上級(jí)擁有下級(jí)的所有權(quán)限(案例分析)
答案是能!
松哥之前寫過類似的文章,但是主要是講了用法,今天我們來看看原理!
本文基于當(dāng)前 Spring Security 5.3.4 來分析,為什么要強(qiáng)調(diào)最新版呢?因?yàn)樵谠?5.0.11 版中,角色繼承配置和現(xiàn)在不一樣。舊版的方案我們現(xiàn)在不討論了,直接來看當(dāng)前最新版是怎么處理的。
1.角色繼承案例
我們先來一個(gè)簡單的權(quán)限案例。
創(chuàng)建一個(gè) Spring Boot 項(xiàng)目,添加 Spring Security 依賴,并創(chuàng)建兩個(gè)測(cè)試用戶,如下:
@Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.inMemoryAuthentication() .withUser("javaboy") .password("{noop}123").roles("admin") .and() .withUser("江南一點(diǎn)雨") .password("{noop}123") .roles("user"); }
然后準(zhǔn)備三個(gè)測(cè)試接口,如下:
@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"; } }
這三個(gè)測(cè)試接口,我們的規(guī)劃是這樣的:
- /hello 是任何人都可以訪問的接口
- /admin/hello 是具有 admin 身份的人才能訪問的接口
- /user/hello 是具有 user 身份的人才能訪問的接口
- 所有 user 能夠訪問的資源,admin 都能夠訪問
注意第四條規(guī)范意味著所有具備 admin 身份的人自動(dòng)具備 user 身份。
接下來我們來配置權(quán)限的攔截規(guī)則,在 Spring Security 的 configure(HttpSecurity http) 方法中,代碼如下:
http.authorizeRequests() .antMatchers("/admin/**").hasRole("admin") .antMatchers("/user/**").hasRole("user") .anyRequest().authenticated() .and() ... ...
這里的匹配規(guī)則我們采用了 Ant 風(fēng)格的路徑匹配符,Ant 風(fēng)格的路徑匹配符在 Spring 家族中使用非常廣泛,它的匹配規(guī)則也非常簡單:
通配符 | 含義 |
---|---|
** | 匹配多層路徑 |
* | 匹配一層路徑 |
? | 匹配任意單個(gè)字符 |
上面配置的含義是:
- 如果請(qǐng)求路徑滿足
/admin/**
格式,則用戶需要具備 admin 角色。 - 如果請(qǐng)求路徑滿足
/user/**
格式,則用戶需要具備 user 角色。 - 剩余的其他格式的請(qǐng)求路徑,只需要認(rèn)證(登錄)后就可以訪問。
注意代碼中配置的三條規(guī)則的順序非常重要,和 Shiro 類似,Spring Security 在匹配的時(shí)候也是按照從上往下的順序來匹配,一旦匹配到了就不繼續(xù)匹配了,所以攔截規(guī)則的順序不能寫錯(cuò)。
如果使用角色繼承,這個(gè)功能很好實(shí)現(xiàn),我們只需要在 SecurityConfig 中添加如下代碼來配置角色繼承關(guān)系即可:
@Bean RoleHierarchy roleHierarchy() { RoleHierarchyImpl hierarchy = new RoleHierarchyImpl(); hierarchy.setHierarchy("ROLE_admin > ROLE_user"); return hierarchy; }
注意,在配置時(shí),需要給角色手動(dòng)加上 ROLE_
前綴。上面的配置表示 ROLE_admin
自動(dòng)具備 ROLE_user
的權(quán)限。
接下來,我們啟動(dòng)項(xiàng)目進(jìn)行測(cè)試。
項(xiàng)目啟動(dòng)成功后,我們首先以 江南一點(diǎn)雨的身份進(jìn)行登錄:
登錄成功后,分別訪問 /hello
,/admin/hello
以及 /user/hello
三個(gè)接口,其中:
/hello
因?yàn)榈卿浐缶涂梢栽L問,這個(gè)接口訪問成功。/admin/hello
需要 admin 身份,所以訪問失敗。/user/hello
需要 user 身份,所以訪問成功。
再以 javaboy 身份登錄,登錄成功后,我們發(fā)現(xiàn) javaboy 也能訪問 /user/hello
這個(gè)接口了,說明我們的角色繼承配置沒問題!
2.原理分析
這里配置的核心在于我們提供了一個(gè) RoleHierarchy 實(shí)例,所以我們的分析就從該類入手。
RoleHierarchy 是一個(gè)接口,該接口中只有一個(gè)方法:
public interface RoleHierarchy { Collection<? extends GrantedAuthority> getReachableGrantedAuthorities( Collection<? extends GrantedAuthority> authorities); }
這個(gè)方法參數(shù) authorities 是一個(gè)權(quán)限集合,從方法名上看方法的返回值是一個(gè)可訪問的權(quán)限集合。
舉個(gè)簡單的例子,假設(shè)角色層次結(jié)構(gòu)是 ROLE_A > ROLE_B > ROLE_C
,現(xiàn)在直接給用戶分配的權(quán)限是 ROLE_A
,但實(shí)際上用戶擁有的權(quán)限有 ROLE_A
、ROLE_B
以及 ROLE_C
。
getReachableGrantedAuthorities 方法的目的就是是根據(jù)角色層次定義,將用戶真正可以觸達(dá)的角色解析出來。
RoleHierarchy 接口有兩個(gè)實(shí)現(xiàn)類,如下圖:
- NullRoleHierarchy 這是一個(gè)空的實(shí)現(xiàn),將傳入的參數(shù)原封不動(dòng)返回。
- RoleHierarchyImpl 這是我們上文所使用的實(shí)現(xiàn),這個(gè)會(huì)完成一些解析操作。
我們來重點(diǎn)看下 RoleHierarchyImpl 類。
這個(gè)類中實(shí)際上就四個(gè)方法 setHierarchy
、getReachableGrantedAuthorities
、buildRolesReachableInOneStepMap
以及 buildRolesReachableInOneOrMoreStepsMap
,我們來逐個(gè)進(jìn)行分析。
首先是我們一開始調(diào)用的 setHierarchy 方法,這個(gè)方法用來設(shè)置角色層級(jí)關(guān)系:
public void setHierarchy(String roleHierarchyStringRepresentation) { this.roleHierarchyStringRepresentation = roleHierarchyStringRepresentation; if (logger.isDebugEnabled()) { logger.debug("setHierarchy() - The following role hierarchy was set: " + roleHierarchyStringRepresentation); } buildRolesReachableInOneStepMap(); buildRolesReachableInOneOrMoreStepsMap(); }
用戶傳入的字符串變量設(shè)置給 roleHierarchyStringRepresentation 屬性,然后通過 buildRolesReachableInOneStepMap 和 buildRolesReachableInOneOrMoreStepsMap 方法完成對(duì)角色層級(jí)的解析。
buildRolesReachableInOneStepMap 方法用來將角色關(guān)系解析成一層一層的形式。我們來看下它的源碼:
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); } } }
首先大家看到,按照換行符來解析用戶配置的多個(gè)角色層級(jí),這是什么意思呢?
我們前面案例中只是配置了 ROLE_admin > ROLE_user
,如果你需要配置多個(gè)繼承關(guān)系,怎么配置呢?多個(gè)繼承關(guān)系用 \n
隔開即可,如下 ROLE_A > ROLE_B \n ROLE_C > ROLE_D
。還有一種情況,如果角色層級(jí)關(guān)系是連續(xù)的,也可以這樣配置 ROLE_A > ROLE_B > ROLE_C > ROLE_D
。
所以這里先用 \n
將多層繼承關(guān)系拆分開形成一個(gè)數(shù)組,然后對(duì)數(shù)組進(jìn)行遍歷。
在具體遍歷中,通過 >
將角色關(guān)系拆分成一個(gè)數(shù)組,然后對(duì)數(shù)組進(jìn)行解析,高一級(jí)的角色作為 key,低一級(jí)的角色作為 value。
代碼比較簡單,最終的解析出來存入 rolesReachableInOneStepMap 中的層級(jí)關(guān)系是這樣的:
假設(shè)角色繼承關(guān)系是 ROLE_A > ROLE_B \n ROLE_C > ROLE_D \n ROLE_C > ROLE_E
,Map 中的數(shù)據(jù)是這樣:
- A–>B
- C–>[D,E]
假設(shè)角色繼承關(guān)系是 ROLE_A > ROLE_B > ROLE_C > ROLE_D
,Map 中的數(shù)據(jù)是這樣:
- A–>B
- B–>C
- C–>D
這是 buildRolesReachableInOneStepMap 方法解析出來的 rolesReachableInOneStepMap 集合。
接下來的 buildRolesReachableInOneOrMoreStepsMap 方法則是對(duì) rolesReachableInOneStepMap 集合進(jìn)行再次解析,將角色的繼承關(guān)系拉平。
例如 rolesReachableInOneStepMap 中保存的角色繼承關(guān)系如下:
- A–>B
- B–>C
- C–>D
經(jīng)過 buildRolesReachableInOneOrMoreStepsMap 方法解析之后,新的 Map 中保存的數(shù)據(jù)如下:
- A–>[B、C、D]
- B–>[C、D]
- C–>D
這樣解析完成后,每一個(gè)角色可以觸達(dá)到的角色就一目了然了。
我們來看下 buildRolesReachableInOneOrMoreStepsMap 方法的實(shí)現(xiàn)邏輯:
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); } }
這個(gè)方法還比較巧妙。首先根據(jù) roleName 從 rolesReachableInOneStepMap 中獲取對(duì)應(yīng)的 rolesToVisitSet,這個(gè) rolesToVisitSet 是一個(gè) Set 集合,對(duì)其進(jìn)行遍歷,將遍歷結(jié)果添加到 visitedRolesSet 集合中,如果 rolesReachableInOneStepMap 集合的 key 不包含當(dāng)前讀取出來的 lowerRole,說明這個(gè) lowerRole 就是整個(gè)角色體系中的最底層,直接 continue。否則就把 lowerRole 在 rolesReachableInOneStepMap 中對(duì)應(yīng)的 value 拿出來繼續(xù)遍歷。
最后將遍歷結(jié)果存入 rolesReachableInOneOrMoreStepsMap 集合中即可。
這個(gè)方法有點(diǎn)繞,小伙伴們可以自己打個(gè)斷點(diǎn)品一下。
看了上面的分析,小伙伴們可能發(fā)現(xiàn)了,其實(shí)角色繼承,最終還是拉平了去對(duì)比。
我們定義的角色有層級(jí),但是代碼中又將這種層級(jí)拉平了,方便后續(xù)的比對(duì)。
最后還有一個(gè) getReachableGrantedAuthorities 方法,根據(jù)傳入的角色分析出其可能潛在包含的一些角色:
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; }
這個(gè)方法的邏輯比較直白,就是從 rolesReachableInOneOrMoreStepsMap 集合中查詢出當(dāng)前角色真正可訪問的角色信息。
3.RoleHierarchyVoter
getReachableGrantedAuthorities 方法將在 RoleHierarchyVoter 投票器中被調(diào)用。
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()); } }
關(guān)于 Spring Security 投票器,將是另外一個(gè)故事,松哥將在下篇文章中和小伙伴們分享投票器和決策器~
4.小結(jié)
到此這篇關(guān)于Spring Security 中如何讓上級(jí)擁有下級(jí)的所有權(quán)限的文章就介紹到這了,更多相關(guān)Spring Security上級(jí)擁有下級(jí)的所有權(quán)限內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- SpringBoot2.0 整合 SpringSecurity 框架實(shí)現(xiàn)用戶權(quán)限安全管理方法
- SpringBoot+Spring Security+JWT實(shí)現(xiàn)RESTful Api權(quán)限控制的方法
- spring security動(dòng)態(tài)配置url權(quán)限的2種實(shí)現(xiàn)方法
- SpringBoot+Vue前后端分離,使用SpringSecurity完美處理權(quán)限問題的解決方法
- Spring Boot中使用 Spring Security 構(gòu)建權(quán)限系統(tǒng)的示例代碼
- 話說Spring Security權(quán)限管理(源碼詳解)
- java中自定義Spring Security權(quán)限控制管理示例(實(shí)戰(zhàn)篇)
- Spring security實(shí)現(xiàn)權(quán)限管理示例
相關(guān)文章
spring中使用mybatis plus連接sqlserver的方法實(shí)現(xiàn)
這篇文章主要介紹了spring中使用mybatis plus連接sqlserver的方法實(shí)現(xiàn),文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-12-12java向mysql插入數(shù)據(jù)亂碼問題的解決方法
這篇文章主要為大家詳細(xì)介紹了java向mysql插入數(shù)據(jù)亂碼問題的解決方法,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-09-09Spring計(jì)時(shí)器StopWatch使用示例
這篇文章主要介紹了Spring計(jì)時(shí)器StopWatch使用示例,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-05-05springboot aspect通過@annotation進(jìn)行攔截的實(shí)例代碼詳解
這篇文章主要介紹了springboot aspect通過@annotation進(jìn)行攔截的方法,本文通過實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-08-08關(guān)于Java從本地文件復(fù)制到網(wǎng)絡(luò)文件上傳
這篇文章主要介紹了關(guān)于Java從本地文件復(fù)制到網(wǎng)絡(luò)文件上傳,File?和?IO?流其實(shí)是很相似的,都是將文件從一個(gè)地方轉(zhuǎn)移到另一個(gè)地方,這也是流的特點(diǎn)之一,需要的朋友可以參考下2023-04-04SpringBoot使用JavaMailSender實(shí)現(xiàn)發(fā)送郵件+Excel附件
項(xiàng)目審批完畢后,需要發(fā)送郵件通知相關(guān)人員,并且要附帶數(shù)據(jù)庫表生成的Excel表格,這就要求不光是郵件發(fā)送功能,還要臨時(shí)生成Excel表格做為附件,本文詳細(xì)介紹了SpringBoot如何使用JavaMailSender實(shí)現(xiàn)發(fā)送郵件+Excel附件,需要的朋友可以參考下2023-10-10