1. 程式人生 > 程式設計 >Spring Security 中如何讓上級擁有下級的所有許可權(案例分析)

Spring Security 中如何讓上級擁有下級的所有許可權(案例分析)

答案是能!

鬆哥之前寫過類似的文章,但是主要是講了用法,今天我們來看看原理!

本文基於當前 Spring Security 5.3.4 來分析,為什麼要強調最新版呢?因為在在 5.0.11 版中,角色繼承配置和現在不一樣。舊版的方案我們現在不討論了,直接來看當前最新版是怎麼處理的。

1.角色繼承案例

我們先來一個簡單的許可權案例。

建立一個 Spring Boot 專案,新增 Spring Security 依賴,並建立兩個測試使用者,如下:

@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";
 }
}

這三個測試介面,我們的規劃是這樣的:

  • /hello 是任何人都可以訪問的介面
  • /admin/hello 是具有 admin 身份的人才能訪問的介面
  • /user/hello 是具有 user 身份的人才能訪問的介面
  • 所有 user 能夠訪問的資源,admin 都能夠訪問

注意第四條規範意味著所有具備 admin 身份的人自動具備 user 身份。

接下來我們來配置許可權的攔截規則,在 Spring Security 的 configure(HttpSecurity http) 方法中,程式碼如下:

http.authorizeRequests()
 .antMatchers("/admin/**").hasRole("admin")
 .antMatchers("/user/**").hasRole("user")
 .anyRequest().authenticated()
 .and()
 ...
 ...

這裡的匹配規則我們採用了 Ant 風格的路徑匹配符,Ant 風格的路徑匹配符在 Spring 家族中使用非常廣泛,它的匹配規則也非常簡單:

萬用字元 含義
** 匹配多層路徑
* 匹配一層路徑
? 匹配任意單個字元

上面配置的含義是:

  • 如果請求路徑滿足 /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 的許可權。

接下來,我們啟動專案進行測試。

專案啟動成功後,我們首先以 江南一點雨的身份進行登入:

Spring Security 中如何讓上級擁有下級的所有許可權(案例分析)

登入成功後,分別訪問 /hello/admin/hello 以及 /user/hello 三個介面,其中:

  • /hello 因為登入後就可以訪問,這個介面訪問成功。
  • /admin/hello 需要 admin 身份,所以訪問失敗。
  • /user/hello 需要 user 身份,所以訪問成功。

再以 javaboy 身份登入,登入成功後,我們發現 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_AROLE_B 以及 ROLE_C

getReachableGrantedAuthorities 方法的目的就是是根據角色層次定義,將使用者真正可以觸達的角色解析出來。

RoleHierarchy 介面有兩個實現類,如下圖:

Spring Security 中如何讓上級擁有下級的所有許可權(案例分析)

  • NullRoleHierarchy 這是一個空的實現,將傳入的引數原封不動返回。
  • RoleHierarchyImpl 這是我們上文所使用的實現,這個會完成一些解析操作。

我們來重點看下 RoleHierarchyImpl 類。

這個類中實際上就四個方法 setHierarchygetReachableGrantedAuthoritiesbuildRolesReachableInOneStepMap 以及 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 中的資料是這樣:

  • 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 拿出來繼續遍歷。

最後將遍歷結果存入 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 集合中查詢出當前角色真正可訪問的角色資訊。

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上級擁有下級的所有許可權內容請搜尋我們以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援我們!