答案是能!java
鬆哥以前寫過相似的文章,可是主要是講了用法,今天咱們來看看原理!數組
本文基於當前 Spring Security 5.3.4 來分析,爲何要強調最新版呢?由於在在 5.0.11 版中,角色繼承配置和如今不同。舊版的方案咱們如今不討論了,直接來看當前最新版是怎麼處理的。app
咱們先來一個簡單的權限案例。ide
建立一個 Spring Boot 項目,添加 Spring Security 依賴,並建立兩個測試用戶,以下:oop
@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"; } }
這三個測試接口,咱們的規劃是這樣的:ui
注意第四條規範意味着全部具有 admin 身份的人自動具有 user 身份。this
接下來咱們來配置權限的攔截規則,在 Spring Security 的 configure(HttpSecurity http) 方法中,代碼以下:spa
http.authorizeRequests() .antMatchers("/admin/**").hasRole("admin") .antMatchers("/user/**").hasRole("user") .anyRequest().authenticated() .and() ... ...
這裏的匹配規則咱們採用了 Ant 風格的路徑匹配符,Ant 風格的路徑匹配符在 Spring 家族中使用很是普遍,它的匹配規則也很是簡單:debug
通配符 | 含義 |
---|---|
** |
匹配多層路徑 |
* |
匹配一層路徑 |
? |
匹配任意單個字符 |
上面配置的含義是:
/admin/**
格式,則用戶須要具有 admin 角色。/user/**
格式,則用戶須要具有 user 角色。注意代碼中配置的三條規則的順序很是重要,和 Shiro 相似,Spring Security 在匹配的時候也是按照從上往下的順序來匹配,一旦匹配到了就不繼續匹配了,因此攔截規則的順序不能寫錯。
若是使用角色繼承,這個功能很好實現,咱們只須要在 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 身份登陸,登陸成功後,咱們發現 javaboy 也能訪問 /user/hello
這個接口了,說明咱們的角色繼承配置沒問題!
這裏配置的核心在於咱們提供了一個 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 接口有兩個實現類,以下圖:
咱們來重點看下 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
。還有一種狀況,若是角色層級關係是連續的,也能夠這樣配置 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 中的數據是這樣:
假設角色繼承關係是 ROLE_A > ROLE_B > ROLE_C > ROLE_D
,Map 中的數據是這樣:
這是 buildRolesReachableInOneStepMap 方法解析出來的 rolesReachableInOneStepMap 集合。
接下來的 buildRolesReachableInOneOrMoreStepsMap 方法則是對 rolesReachableInOneStepMap 集合進行再次解析,將角色的繼承關係拉平。
例如 rolesReachableInOneStepMap 中保存的角色繼承關係以下:
通過 buildRolesReachableInOneOrMoreStepsMap 方法解析以後,新的 Map 中保存的數據以下:
這樣解析完成後,每個角色能夠觸達到的角色就一目瞭然了。
咱們來看下 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 拿出來繼續遍歷。
最後將遍歷結果存入 rolesReachableInOneOrMoreStepsMap 集合中便可。
這個方法有點繞,小夥伴們能夠本身打個斷點品一下。
看了上面的分析,小夥伴們可能發現了,其實角色繼承,最終仍是拉平了去對比。
咱們定義的角色有層級,可是代碼中又將這種層級拉平了,方便後續的比對。
最後還有一個 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 集合中查詢出當前角色真正可訪問的角色信息。
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 投票器,將是另一個故事,鬆哥將在下篇文章中和小夥伴們分享投票器和決策器~
好啦,今天就和小夥伴們簡簡單單聊一下角色繼承的問題,感興趣的小夥伴能夠本身試一下~若是以爲有收穫,記得點個在看鼓勵下鬆哥哦~