Spring Security 許可權管理的投票器與表決機制

beluga發表於2020-09-16

今天我們們來聊一聊 Spring Security 中的表決機制與投票器。

當使用者想訪問 Spring Security 中一個受保護的資源時,使用者具備一些角色,該資源的訪問也需要一些角色,在比對使用者具備的角色和資源需要的角色時,就會用到投票器和表決機制。

當使用者想要訪問某一個資源時,投票器根據使用者的角色投出贊成或者反對票,表決方式則根據投票器的結果進行表決。

在 Spring Security 中,預設提供了三種表決機制,當然,我們也可以不用系統提供的表決機制和投票器,而是完全自己來定義,這也是可以的。

本文松哥將和大家重點介紹三種表決機制和預設的投票器。

1.投票器

先來看投票器。

在 Spring Security 中,投票器是由 AccessDecisionVoter 介面來規範的,我們來看下 AccessDecisionVoter 介面的實現:

可以看到,投票器的實現有好多種,我們可以選擇其中一種或多種投票器,也可以自定義投票器,預設的投票器是 WebExpressionVoter。

我們來看 AccessDecisionVoter 的定義:

public interface AccessDecisionVoter<S> {
	int ACCESS_GRANTED = 1;
	int ACCESS_ABSTAIN = 0;
	int ACCESS_DENIED = -1;
	boolean supports(ConfigAttribute attribute);
	boolean supports(Class<?> clazz);
	int vote(Authentication authentication, S object,
			Collection<ConfigAttribute> attributes);}123456789

我稍微解釋下:

  1. 首先一上來定義了三個常量,從常量名字中就可以看出每個常量的含義,1 表示贊成;0 表示棄權;-1 表示拒絕。
  2. 兩個 supports 方法用來判斷投票器是否支援當前請求。
  3. vote 則是具體的投票方法。在不同的實現類中實現。三個引數,authentication 表示當前登入主體;object 是一個 ilterInvocation,裡邊封裝了當前請求;attributes 表示當前所訪問的介面所需要的角色集合。

我們來分別看下幾個投票器的實現。

1.1 RoleVoter

RoleVoter 主要用來判斷當前請求是否具備該介面所需要的角色,我們來看下其 vote 方法:

public int vote(Authentication authentication, Object object,
		Collection<ConfigAttribute> attributes) {
	if (authentication == null) {
		return ACCESS_DENIED;
	}
	int result = ACCESS_ABSTAIN;
	Collection<? extends GrantedAuthority> authorities = extractAuthorities(authentication);
	for (ConfigAttribute attribute : attributes) {
		if (this.supports(attribute)) {
			result = ACCESS_DENIED;
			for (GrantedAuthority authority : authorities) {
				if (attribute.getAttribute().equals(authority.getAuthority())) {
					return ACCESS_GRANTED;
				}
			}
		}
	}
	return result;}12345678910111213141516171819

這個方法的判斷邏輯很簡單,如果當前登入主體為 null,則直接返回 ACCESS_DENIED 表示拒絕訪問;否則就從當前登入主體 authentication 中抽取出角色資訊,然後和 attributes 進行對比,如果具備 attributes 中所需角色的任意一種,則返回 ACCESS_GRANTED 表示允許訪問。例如 attributes 中的角色為 [a,b,c],當前使用者具備 a,則允許訪問,不需要三種角色同時具備。

另外還有一個需要注意的地方,就是 RoleVoter 的 supports 方法,我們來看下:

public class RoleVoter implements AccessDecisionVoter<Object> {
	private String rolePrefix = "ROLE_";
	public String getRolePrefix() {
		return rolePrefix;
	}
	public void setRolePrefix(String rolePrefix) {
		this.rolePrefix = rolePrefix;
	}
	public boolean supports(ConfigAttribute attribute) {
		if ((attribute.getAttribute() != null)
				&& attribute.getAttribute().startsWith(getRolePrefix())) {
			return true;
		}
		else {
			return false;
		}
	}
	public boolean supports(Class<?> clazz) {
		return true;
	}}123456789101112131415161718192021

可以看到,這裡涉及到了一個 rolePrefix 字首,這個字首是  ROLE_,在 supports 方法中,只有主體角色字首是  ROLE_,這個 supoorts 方法才會返回 true,這個投票器才會生效。

1.2 RoleHierarchyVoter

RoleHierarchyVoter 是 RoleVoter 的一個子類,在 RoleVoter 角色判斷的基礎上,引入了角色分層管理,也就是角色繼承,關於角色繼承,小夥伴們可以參考松哥之前的文章( Spring Security 中如何讓上級擁有下級的所有許可權?)。

RoleHierarchyVoter 類的 vote 方法和 RoleVoter 一致,唯一的區別在於 RoleHierarchyVoter 類重寫了 extractAuthorities 方法。

@OverrideCollection<? extends GrantedAuthority> extractAuthorities(
		Authentication authentication) {
	return roleHierarchy.getReachableGrantedAuthorities(authentication			.getAuthorities());}123456

角色分層之後,需要透過 getReachableGrantedAuthorities 方法獲取實際具備的角色,具體請參考: Spring Security 中如何讓上級擁有下級的所有許可權? 一文。

1.3 WebExpressionVoter

這是一個基於表示式許可權控制的投票器,松哥後面專門花點時間和小夥伴們聊一聊基於表示式的許可權控制,這裡我們先不做過多展開,簡單看下它的 vote 方法:

public int vote(Authentication authentication, FilterInvocation fi,
		Collection<ConfigAttribute> attributes) {
	assert authentication != null;
	assert fi != null;
	assert attributes != null;
	WebExpressionConfigAttribute weca = findConfigAttribute(attributes);
	if (weca == null) {
		return ACCESS_ABSTAIN;
	}
	EvaluationContext ctx = expressionHandler.createEvaluationContext(authentication,
			fi);
	ctx = weca.postProcess(ctx, fi);
	return ExpressionUtils.evaluateAsBoolean(weca.getAuthorizeExpression(), ctx) ? ACCESS_GRANTED			: ACCESS_DENIED;}123456789101112131415

如果你熟練使用 SpEL 的話,這段程式碼應該說還是很好理解的,不過根據我的經驗,實際工作中用到 SpEL 場景雖然有,但是不多,所以可能有很多小夥伴並不瞭解 SpEL 的用法,這個需要小夥伴們自行復習下,我也給大家推薦一篇還不錯的文章: https://www.cnblogs.com/larryzeal/p/5964621.html

這裡程式碼實際上就是根據傳入的 attributes 屬性構建 weca 物件,然後根據傳入的 authentication 引數構建 ctx 物件,最後呼叫 evaluateAsBoolean 方法去判斷許可權是否匹配。

上面介紹這三個投票器是我們在實際開發中使用較多的三個。

1.4 其他

另外還有幾個比較冷門的投票器,松哥也稍微說下,小夥伴們瞭解下。

Jsr250Voter

處理 Jsr-250 許可權註解的投票器,如  @PermitAll@DenyAll 等。

AuthenticatedVoter

AuthenticatedVoter 用於判斷 ConfigAttribute 上是否擁有 IS_AUTHENTICATED_FULLY、IS_AUTHENTICATED_REMEMBERED、IS_AUTHENTICATED_ANONYMOUSLY 三種角色。

IS_AUTHENTICATED_FULLY 表示當前認證使用者必須是透過使用者名稱/密碼的方式認證的,透過 RememberMe 的方式認證無效。

IS_AUTHENTICATED_REMEMBERED 表示當前登入使用者必須是透過 RememberMe 的方式完成認證的。

IS_AUTHENTICATED_ANONYMOUSLY 表示當前登入使用者必須是匿名使用者。

當專案引入 RememberMe 並且想區分不同的認證方式時,可以考慮這個投票器。

AbstractAclVoter

提供編寫域物件 ACL 選項的幫助方法,沒有繫結到任何特定的 ACL 系統。

PreInvocationAuthorizationAdviceVoter

使用 @PreFilter 和 @PreAuthorize 註解處理的許可權,透過 PreInvocationAuthorizationAdvice 來授權。

當然,如果這些投票器不能滿足需求,也可以自定義。

2.表決機制

一個請求不一定只有一個投票器,也可能有多個投票器,所以在投票器的基礎上我們還需要表決機制。

表決相關的類主要是三個:

  • AffirmativeBased
  • ConsensusBased
  • UnanimousBased

他們的繼承關係如上圖。

三個決策器都會把專案中的所有投票器呼叫一遍,預設使用的決策器是 AffirmativeBased。

三個決策器的區別如下:

  • AffirmativeBased:有一個投票器同意了,就透過。
  • ConsensusBased:多數投票器同意就透過,平局的話,則看 allowIfEqualGrantedDeniedDecisions 引數的取值。
  • UnanimousBased 所有投票器都同意,請求才透過。

這裡的具體判斷邏輯比較簡單,松哥就不貼原始碼了,感興趣的小夥伴可以自己看看。

3.在哪裡配置

當我們使用基於表示式的許可權控制時,像下面這樣:

http.authorizeRequests()
        .antMatchers("/admin/**").hasRole("admin")
        .antMatchers("/user/**").hasRole("user")
        .anyRequest().fullyAuthenticated()1234

那麼預設的投票器和決策器是在 AbstractInterceptUrlConfigurer#createDefaultAccessDecisionManager 方法中配置的:

private AccessDecisionManager createDefaultAccessDecisionManager(H http) {
	AffirmativeBased result = new AffirmativeBased(getDecisionVoters(http));
	return postProcess(result);}List<AccessDecisionVoter<?>> getDecisionVoters(H http) {
	List<AccessDecisionVoter<?>> decisionVoters = new ArrayList<>();
	WebExpressionVoter expressionVoter = new WebExpressionVoter();
	expressionVoter.setExpressionHandler(getExpressionHandler(http));
	decisionVoters.add(expressionVoter);
	return decisionVoters;}1234567891011

這裡就可以看到預設的決策器和投票器,並且決策器 AffirmativeBased 物件建立好之後,還呼叫 postProcess 方法註冊到 Spring 容器中去了,結合松哥本系列前面的文章,大家知道,如果我們想要修改該物件就非常容易了:

http.authorizeRequests()
        .antMatchers("/admin/**").hasRole("admin")
        .antMatchers("/user/**").hasRole("user")
        .anyRequest().fullyAuthenticated()
        .withObjectPostProcessor(new ObjectPostProcessor<AffirmativeBased>() {
            @Override
            public <O extends AffirmativeBased> O postProcess(O object) {
                List<AccessDecisionVoter<?>> decisionVoters = new ArrayList<>();
                decisionVoters.add(new RoleHierarchyVoter(roleHierarchy()));
                AffirmativeBased affirmativeBased = new AffirmativeBased(decisionVoters);
                return (O) affirmativeBased;
            }
        })
        .and()
        .csrf()
        .disable();12345678910111213141516

這裡只是給大家一個演示,正常來說我們是不需要這樣修改的。當我們使用不同的許可權配置方式時,會有自動配置對應的投票器和決策器。或者我們手動配置投票器和決策器,如果是系統配置好的,大部分情況下並不需要我們修改。


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69981976/viewspace-2721737/,如需轉載,請註明出處,否則將追究法律責任。

相關文章