1. 程式人生 > 程式設計 >Spring Security原理介紹、原始碼解析——授權過程

Spring Security原理介紹、原始碼解析——授權過程

流程簡述

當我們成功登入,獲取access_token,即可使用該token來訪問有許可權的介面。如上文所講,JwtAuthenticationFilteraccess_token轉化為系統可識別的Authentication放入安全上下文, 則來到最後一個過濾器FilterSecurityInterceptor,該過濾則是判斷請求是否擁有許可權。

public class FilterSecurityInterceptor extends AbstractSecurityInterceptor implements Filter {
  
  public void doFilter(ServletRequest request,ServletResponse response,FilterChain chain)
throws IOException,ServletException
{ FilterInvocation fi = new FilterInvocation(request,response,chain); invoke(fi); } public void invoke(FilterInvocation fi) throws IOException,ServletException { if ((fi.getRequest() != null) && (fi.getRequest().getAttribute(FILTER_APPLIED) != null
) && observeOncePerRequest) { // filter already applied to this request and user wants us to observe // once-per-request handling,so don't re-do security checking fi.getChain().doFilter(fi.getRequest(),fi.getResponse()); } else { // first time this request being called,so perform security checking
if (fi.getRequest() != null && observeOncePerRequest) { fi.getRequest().setAttribute(FILTER_APPLIED,Boolean.TRUE); } // 請求之前的工作,也就是真正的許可權認證的過程 InterceptorStatusToken token = super.beforeInvocation(fi); try { // 請求真正的controller fi.getChain().doFilter(fi.getRequest(),fi.getResponse()); } finally { super.finallyInvocation(token); } // 請求後的工作 super.afterInvocation(token,null); } } } 複製程式碼

FilterSecurityInterceptor的主體方法依舊在doFilter中,而其中主要的方法為invoke(),大約分為三個步驟:

  1. beforeInvocation(fi); 驗證Context中的Authentication和目標url所需許可權是否匹配,匹配則通過,不通過則丟擲異常。
  2. fi.getChain().doFilter(fi.getRequest(),fi.getResponse()); 在此可以看做是,真正去訪問目標Controller。
  3. afterInvocation(token,null); 獲取請求後的操作。

首先來看看beforeInvocation()

beforeInvocation

abstract class AbstractSecurityInterceptor {
  protected InterceptorStatusToken beforeInvocation(Object object) {
     // 獲取目標url的許可權內容,這些內容可以從configuration中獲取也可以用MetadataSource中獲取
     Collection<ConfigAttribute> attributes = this.obtainSecurityMetadataSource().getAttributes(object);
     // ……省略
    
  	 Authentication authenticated = authenticateIfRequired();
  
  	 // Attempt authorization
  	 try {
  	    // AccessDecisionManager用於驗證Authentication中的許可權和目標url所需許可權是否匹配,如果不匹配則丟擲AccessDeniedException異常
  	    this.accessDecisionManager.decide(authenticated,object,attributes);
  	 }
  	 catch (AccessDeniedException accessDeniedException) {
  		publishEvent(new AuthorizationFailureEvent(object,attributes,authenticated,accessDeniedException));
    			throw accessDeniedException;
  	 }
  
  	 // Attempt to run as a different user
  	 Authentication runAs = this.runAsManager.buildRunAs(authenticated,attributes);
  	 
  	 // 下一步則是生成InterceptorStatusToken,用於AfterInvocation步驟。有興趣可以自己看
  	 if (runAs == null) {
  	   // no further work post-invocation
  		return new InterceptorStatusToken(SecurityContextHolder.getContext(),false,object);
  	 }
  	 else {
  		SecurityContext origCtx = SecurityContextHolder.getContext();
  		SecurityContextHolder.setContext(SecurityContextHolder.createEmptyContext());
  		SecurityContextHolder.getContext().setAuthentication(runAs);
  		// need to revert to token.Authenticated post-invocation
  		return new InterceptorStatusToken(origCtx,true,object);
  	 }
  }
}
複製程式碼
  1. Collection<ConfigAttribute> attributes = this.obtainSecurityMetadataSource().getAttributes(object);獲取目標url所需要的許可權, 該類實現FilterInvocationSecurityMetadataSource介面的方法。而配置url許可權也可以從WebSecurityConfig中的configuration方法配置。
  2. this.accessDecisionManager.decide(authenticated,attributes); 判斷Authentication中的許可權目標url所需許可權是否匹配,匹配則通過;不匹配則丟擲AccessDeniedException異常。 該方法來自AbstractAccessDecisionManager的實現類,系統預設實現為AffirmativeBased
  3. new InterceptorStatusToken(SecurityContextHolder.getContext(),false,object); 實現InterceptorStatusToken並返回,包括引數中的資訊,如安全上下文、目標url所需許可權、原始的訪問請求。

之後則訪問目標Controller,獲取真正的請求內容。

afterInvocation

當我們啟用了@PreAuthorize()@PostAuthorize()註解的時候則會AfterInvocationManger,進而有以下驗證邏輯。

abstract class AbstractSecurityInterceptor {
  protected Object afterInvocation(InterceptorStatusToken token,Object returnedObject) {
    if (token == null) {
  	  // public object
  	  return returnedObject;
  	}
  
  	finallyInvocation(token); // continue to clean in this method for passivity
  
  	if (afterInvocationManager != null) {
  	// Attempt after invocation handling
  	  try {
  		returnedObject = afterInvocationManager.decide(token.getSecurityContext()
  		  .getAuthentication(),token.getSecureObject(),token
  		  .getAttributes(),returnedObject);
  	  }
  	  catch (AccessDeniedException accessDeniedException) {
  		AuthorizationFailureEvent event = new AuthorizationFailureEvent(
  		  token.getSecureObject(),token.getAttributes(),token
  			.getSecurityContext().getAuthentication(),accessDeniedException);
  		publishEvent(event);
  		throw accessDeniedException;
  	  }
  	}
  	return returnedObject;
  }
}
複製程式碼

以下程式碼則是包含AfterInvocationManager具體的實現。

public class GlobalMethodSecurityConfiguration {
  protected AfterInvocationManager afterInvocationManager() {
    if (prePostEnabled()) {
  	  AfterInvocationProviderManager invocationProviderManager = new AfterInvocationProviderManager();
  		ExpressionBasedPostInvocationAdvice postAdvice = new ExpressionBasedPostInvocationAdvice(
  	    	getExpressionHandler());
  		PostInvocationAdviceProvider postInvocationAdviceProvider = new PostInvocationAdviceProvider(
  			postAdvice);
  		List<AfterInvocationProvider> afterInvocationProviders = new ArrayList<>();
  		afterInvocationProviders.add(postInvocationAdviceProvider);
  		invocationProviderManager.setProviders(afterInvocationProviders);
  		return invocationProviderManager;
  	  }
  	return null;
  }
}
複製程式碼

我們可以做些什麼?

  1. 實現FilterInvocationSecurityMetadataSource,用於啟動時載入url所需的許可權,這樣就不用在configuration或者註解中將目標url許可權‘寫死’。 可以參照本例所寫的實現MyFilterInvocationSecurityMetadataSource

  2. 過載AbstractAccessDecisionManager,根據業務需要重寫,請求目標許可權和Authentication中許可權的驗證過程. 舉個例子,Spring Security中預設的RBAC,即,許可權認證都是根據角色判斷,固定角色只能訪問固定介面。 現在我們需要ACL許可權模型,使用者A許可權為1,使用者B許可權為5,使用者C許可權為9,介面a需要許可權6,則使用者C可以訪問, 而使用者A、B不能訪問,就是說許可權大的可以訪問許可權小的介面,如果需要改變許可權模型則過載該類即可。

總結

授權過程主要有哪些?

  1. 獲取請求目標所需許可權,從FilterInvocationSecurityMetadataSource介面的實現類獲取。
  2. 對比安全上下文中Authentication中的許可權是否匹配,在AbstractAccessDecisionManager的實現類中比較。

連結

文章涉及到程式碼已傳到gitee上,供大家參考: gitee.com/yangzijing/…

Spring Security原始碼龐大且複雜,本人水平有限,文章難免有錯漏、表述不清之處,請大家支出。歡迎交流,希望和大家共同進步。