Spring Security教程 Vol 8. AccessDecisionVoter元件介紹

廢柴大叔阿基拉發表於2019-04-20

第八期 AccessDecisionVoter元件介紹

這一期主要我們將介紹訪問控制三劍客負責對授權規則做角色的元件——AccessDecisionVoter介面。以及對Spring Security預設提供的幾個基礎AccessDecisionVoter實現類做一個詳細的說明,最後我們將會客製化一個基於時間的AccessDecisionVoter實現用於實戰說明。

  • AccessDecisionVoter介面說明
  • Spring Security的AccessDecisionVoter
  • 客製化例項:基於時間的AccessDecisionVoter

一、AccessDecisionVoter介面說明

AccessDecisionVoter介面說明
AccessDecisionVoter主要的職責就是對它所對應的訪問規則作出判斷,當前的訪問規則是否可以得到授權。 AccessDecisionVoter介面的主要方法其實與之前的AuthenticationProvider非常的相似。

	boolean supports(ConfigAttribute attribute);

	int vote(Authentication authentication, S object,
			Collection<ConfigAttribute> attributes);
複製程式碼
  • supports方法用於判斷對於當前ConfigAttribute訪問規則是否支援;
  • 如果支援的情況下,vote方法對其進行判斷投票返回對應的授權結果。 最終的授權結果一共有三種,分別是同意、棄權和反對。說實話這個規則和聯合國安理會投票差不多性質。當前一個訪問可能存在多個規則的情況下,每一個AccessDecisionVoter投出自己的那一票,最終的投票結果是還是要看當前的投票規則,比如是超過1/3還是要過半數。而投票規則的判斷則是被放置了在了AccessDecisionManager進行完成。
	int ACCESS_GRANTED = 1;
	int ACCESS_ABSTAIN = 0;
	int ACCESS_DENIED = -1;
複製程式碼

二、 Spring Security的AccessDecisionVoter

通過上面對於AccessDecisionVoter的基本介紹,我們得知了一個設計上的大原則:AccessDecisionVoter的實現是為了滿足對應規則ConfigAttribute。大體上來說AccessDecisionVoter是與ConfigAttribute一一對應的。 讓我們回一下在上一期我們介紹的主要的幾種ConfigAttribute實現:

  • 基於Web表示式的WebExpressionConfigAttribute
  • 基於@Secured註解的SecurityConfig
  • 基於@Pre-@Post註解的PostInvocationExpressionAttribute 我們可以在下圖中輕鬆的找到他門對應的AccessDecisionVoter
    主要的AccessDecisionVoter
    這邊我們重點說一下在客製化場景下被利用的SecurityConfig配置和他預設的兩個AccessDecisionVoter:
  • RoleVoter
  • AuthenticatedVoter 首先,我們來回憶下SecurityConfig的使用形式,即利用@Secured註解編寫一個表示式:
@Secured("ROLE_USER")
@Secured("IS_AUTHENTICATED_ANONYMOUSLY")
複製程式碼

我們瞭解到了AccessDecisionVoterConfigAttribute的關聯關係是通過supports方法進行判斷,我們分別對RoleVoterAuthenticatedVoter的supports方法進行瀏覽:

RoleVoter RoleVoter是Spring Security中預設基於角色規則的核心元件。在UserDetailsService中建立使用者我們都會需要設定對用使用者的角色資訊。在預設配置下使用者的角色資訊都是以"ROLE_"+角色名的形式儲存的。 對應的在RoleVoter的supports方法中會對錶達式是否以'ROLE_'開始作為對應啟用規則的判斷。如果規則表示式是以ROLE_開始的,RoleVoter則會去遍歷對用Authentication是否存在對應的角色,如果存在則返回通過,如果不存在則返回拒絕。

public class RoleVoter implements AccessDecisionVoter<Object> {
	// ~ Instance fields
	// ================================================================================================

	private String rolePrefix = "ROLE_";

	// ~ Methods
	// ========================================================================================================

	public String getRolePrefix() {
		return rolePrefix;
	}

	/**
	 * Allows the default role prefix of <code>ROLE_</code> to be overridden. May be set
	 * to an empty value, although this is usually not desirable.
	 *
	 * @param rolePrefix the new prefix
	 */
	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;
		}
	}
}
複製程式碼

AuthenticatedVoter AuthenticatedVoter的使用場景就比較特殊,他並不是一個基於身份資訊的訪問控制,而是對於對應Auhentication的認證形式的一個判斷。在之前的身份驗證部分我們有了解過,在Spring Security設計中,我們可以銅鼓RememberMeService的方式不使用使用者名稱和密碼,而是通過儲存於Cookie的資訊進行授權登入。在日常工程中,對於一些敏感操作,我們要求當前的使用者並不是一個基於歷史進行授權認證的使用者,比如在進行支付的情況下,如果我們希望使用者是在本次訪問中是通過使用者名稱和密碼進行登入展開的會話操作,而不是一個基於一個月前cookies進行登入都有使用者。在這個場景下我們需要便可以使用@Secured("IS_AUTHENTICATED_FULLY")去限定使用者是一個通過完全驗證的使用者,而不是通過RememberMe方式認證的使用者。 在AuthenticatedVoter的supports方法中,便會判斷當前的表示式是為他所支援的三種認證方法的訪問控制:

  • IS_AUTHENTICATED_FULLY
  • IS_AUTHENTICATED_REMEMBERED
  • IS_AUTHENTICATED_ANONYMOUSLY 如果完全匹配,則會當前的Authentication物件的授權模式進行判斷,返回相應的投票結果。
public class AuthenticatedVoter implements AccessDecisionVoter<Object> {
	// ~ Static fields/initializers
	// =====================================================================================

	public static final String IS_AUTHENTICATED_FULLY = "IS_AUTHENTICATED_FULLY";
	public static final String IS_AUTHENTICATED_REMEMBERED = "IS_AUTHENTICATED_REMEMBERED";
	public static final String IS_AUTHENTICATED_ANONYMOUSLY = "IS_AUTHENTICATED_ANONYMOUSLY";
	// ~ Instance fields
	// ================================================================================================

	private AuthenticationTrustResolver authenticationTrustResolver = new AuthenticationTrustResolverImpl();

	// ~ Methods
	// ========================================================================================================

	private boolean isFullyAuthenticated(Authentication authentication) {
		return (!authenticationTrustResolver.isAnonymous(authentication) && !authenticationTrustResolver
				.isRememberMe(authentication));
	}

	public boolean supports(ConfigAttribute attribute) {
		if ((attribute.getAttribute() != null)
				&& (IS_AUTHENTICATED_FULLY.equals(attribute.getAttribute())
						|| IS_AUTHENTICATED_REMEMBERED.equals(attribute.getAttribute()) || IS_AUTHENTICATED_ANONYMOUSLY
							.equals(attribute.getAttribute()))) {
			return true;
		}
		else {
			return false;
		}
	}
}
複製程式碼

三、 客製化例項:基於時間的AccessDecisionVoter

對於AccessDecisionVoter結構、責任和Spring Security中提供的實現類有了一個基礎的瞭解後。我們通過一個客製化的例項來加強這部分的理解。 我們將客製化一個基於時間的訪問控制,在系統時間的分鐘數是奇數的情況下才可以被訪問,比如10點01分可以訪問,但是10點02分則不可以被訪問。

設計規則

首先,我們對訪問規則進行設計。我們如同RoleVoterAuthenticatedVoter一樣基於@Secured註解的表示式進行擴充套件。我們擬定的規則名為"MINUTE_ODD",當方法級被註解了@Secured("MINUTE_ODD")情況下,表示當前方法只有在滿足系統時間的分鐘數為奇數下才可以被訪問。

客製化MinuteBasedVoter

接下來,我們編寫一個MinuteBasedVoter擴充套件AuthenticatedVoter

public class MinuteBasedVoter implements AccessDecisionVoter {
}
複製程式碼

然後,我們實現對應的suppors方法用於完成我們對我們擬定的規則的判斷。當入參ConfigAttribute 的表示式屬性與我們預設的"MINUTE_ODD"一致時,那麼我們便返回true告知框架,MinuteBasedVoter需要對該規則進行vote的投票操作。

public class MinuteBasedVoter implements AccessDecisionVoter {
    public static final String IS_MINUTE_ODD= "MINUTE_ODD";

    @Override
    public boolean supports(ConfigAttribute attribute) {
        if ((attribute.getAttribute() != null)
                && attribute.getAttribute().equals(IS_MINUTE_ODD)) {
            return true;
        }
        else {
            return false;
        }
    }


    @Override
    public boolean supports(Class clazz) {
        return true;
    }
}
複製程式碼

最後,我們將vote的投票核心業務邏輯完成:當時間為奇數的時候則投贊同票,而在時間為偶數的時候則投一張明確的反對票

    @Override
    public int vote(Authentication authentication, Object object, Collection collection) {
        if(LocalDateTime.now().getMinute() % 2 != 0){
            return ACCESS_GRANTED;

        }else{
            return ACCESS_DENIED;
        }
    }
複製程式碼

Java Config配置

最後,說一下如何將新的AccessDecisionVoter新增到現有的AccessDecisionManager中。我自己也百度了一下了中文世界和英文世界關於這方便的示例已經官方文件,真的是五花八門都有。最常見的是重新組織了一個AccessDecisionManager注入回Spring Security中,我很不推薦自己在方法中去new一個AccessDecisionManager。因為AccessDecisionManager的初始化過程中涉及的不只是AccessDecisionVoter,一不小心可能因為少設定什麼元件就導致一部分預設行為沒被正確的配置上去。 我推薦初學者方法是對於擴充套件Secured這類基於方法級的註解,單獨新建一個Java Config類,然後重寫原有框架中初始化AccessDecisionManager的方法:

@EnableGlobalMethodSecurity(prePostEnabled = true,securedEnabled = true)
@Configuration
public class MethodSecurityConfiguration extends GlobalMethodSecurityConfiguration {
    @Override
    protected AccessDecisionManager accessDecisionManager() {
        AffirmativeBased affirmativeBased = (AffirmativeBased) super.accessDecisionManager();
        affirmativeBased.getDecisionVoters().add(new MinuteBasedVoter());
        return affirmativeBased;
    }
}
複製程式碼

雖然程式碼可能醜、有對型別強轉,相對來說好理解控制很多。 在新增了MethodSecurityConfiguration的Java Config之後,我們在對受到@Secured("MINUTE_ODD")註解限制的controller方式時便會看到以下的投票日誌:

Secure object: ReflectiveMethodInvocation: public java.lang.String Attributes: [MINUTE_ODD]
Voter: org.springframework.security.access.prepost.PreInvocationAuthorizationAdviceVoter@456f4439, returned: 0
Voter: org.springframework.security.access.vote.RoleVoter@38b13fa8, returned: 0
Voter: org.springframework.security.access.vote.AuthenticatedVoter@590fa701, returned: 0
Voter: com.newnil.demo.security.MinuteBasedVoter@135c04e9, returned: 1
Authorization successful
複製程式碼

AccessDecisionVoter元件們依次投票,而因為當前時間是奇數,所以我們的MinuteBasedVoter投出一票值為1的贊同票。

結尾

這一期詳細介紹了AccessDecisionVoter這一為訪問控制提供核心判斷及投票的元件。同時也通過框架預設提供與客製化實現瞭解了其工作原理。 下一期我們將最後一個核心元件AccessDecisionManager是如何對所有AccessDecisionVoter的投票結果進行彙總,以及如何以什麼評價規則告知框架最終的授權結果進行說明。 我們下期再見。

相關文章