1. 程式人生 > >Spring Security詳解(三)認證之核心過濾器

Spring Security詳解(三)認證之核心過濾器

這章主要用來分析Spring Security中的過濾器鏈包含了哪些關鍵的過濾器,並且各自的作用是什麼。

3 核心過濾器

3.1 概述

Filter順序

Spring Security的官方文件向我們提供了filter的順序,無論實際應用中你用到了哪些,整體的順序是保持不變的:

  • ChannelProcessingFilter,重定向到其他協議的過濾器。也就是說如果你訪問的channel錯了,那首先就會在channel之間進行跳轉,如http變為https。
  • SecurityContextPersistenceFilter,請求來臨時在SecurityContextHolder中建立一個SecurityContext,然後在請求結束的時候,清空SecurityContextHolder。並且任何對SecurityContext的改變都可以被copy到HttpSession。
  • ConcurrentSessionFilter,因為它需要使用SecurityContextHolder的功能,而且更新對應session的最後更新時間,以及通過SessionRegistry獲取當前的SessionInformation以檢查當前的session是否已經過期,過期則會呼叫LogoutHandler。
  • 認證處理機制,如UsernamePasswordAuthenticationFilter,CasAuthenticationFilter,BasicAuthenticationFilter等,以至於SecurityContextHolder可以被更新為包含一個有效的Authentication請求。
  • SecurityContextHolderAwareRequestFilter,它將會把HttpServletRequest封裝成一個繼承自HttpServletRequestWrapper的SecurityContextHolderAwareRequestWrapper,同時使用SecurityContext實現了HttpServletRequest中與安全相關的方法。
  • JaasApiIntegrationFilter,如果SecurityContextHolder中擁有的Authentication是一個JaasAuthenticationToken,那麼該Filter將使用包含在JaasAuthenticationToken中的Subject繼續執行FilterChain。
  • RememberMeAuthenticationFilter,如果之前的認證處理機制沒有更新SecurityContextHolder,並且使用者請求包含了一個Remember-Me對應的cookie,那麼一個對應的Authentication將會設給SecurityContextHolder。
  • AnonymousAuthenticationFilter,如果之前的認證機制都沒有更新SecurityContextHolder擁有的Authentication,那麼一個AnonymousAuthenticationToken將會設給SecurityContextHolder。
  • ExceptionTransactionFilter,用於處理在FilterChain範圍內丟擲的AccessDeniedException和AuthenticationException,並把它們轉換為對應的Http錯誤碼返回或者對應的頁面。
  • FilterSecurityInterceptor,保護Web URI,進行許可權認證,並且在訪問被拒絕時丟擲異常。

如果你想知道你的專案都配置了哪些過濾器,可以從Spring Boot的啟動日誌中找到,上一節我們配置了一個基礎的表單登陸的demo,它在啟動時,Spring Security自動配置的過濾器有:

Creating filter chain: o.s.s.web.util.matcher.AnyRequestMatcher@1, 
[o.s.s.web.context.SecurityContextPersistenceFilter@8851ce1, 
o.s.s.web.header.HeaderWriterFilter@6a472566, o.s.s.web.csrf.CsrfFilter@61cd1c71, 
o.s.s.web.authentication.logout.LogoutFilter@5e1d03d7, 
o.s.s.web.authentication.UsernamePasswordAuthenticationFilter@122d6c22, 
o.s.s.web.savedrequest.RequestCacheAwareFilter@5ef6fd7f, 
o.s.s.web.servletapi.SecurityContextHolderAwareRequestFilter@4beaf6bd, 
o.s.s.web.authentication.AnonymousAuthenticationFilter@6edcad64, 
o.s.s.web.session.SessionManagementFilter@5e65afb6, 
o.s.s.web.access.ExceptionTranslationFilter@5b9396d3, 
o.s.s.web.access.intercept.FilterSecurityInterceptor@3c5dbdf8
]

下面我們來依次講解一下幾個重要的過濾器。

3.2 SecurityContextPersistenceFilter

試想一下,如果我們不使用Spring Security,如果儲存使用者資訊呢,大多數情況下會考慮使用Session對吧?在Spring Security中也是如此,使用者在登入過一次之後,後續的訪問便是通過sessionId來識別,從而認為使用者已經被認證。具體在何處存放使用者資訊,便是第一篇文章中提到的SecurityContextHolder;認證相關的資訊是如何被存放到其中的,便是通過SecurityContextPersistenceFilter。上面我們已經提到過,SecurityContextPersistenceFilter的兩個主要作用便是請求來臨時,建立SecurityContext安全上下文資訊和請求結束時清空SecurityContextHolder。在使用NameSpace時,Spring Security預設會將SecurityContext儲存在HttpSession中。
但如果是基於微服務的話,對應在http的無狀態也就意味著不允許存在session。這可以通過setAllowSessionCreation(false) 實現。

原始碼分析

public class SecurityContextPersistenceFilter extends GenericFilterBean {

   static final String FILTER_APPLIED = "__spring_security_scpf_applied";
   //安全上下文儲存的倉庫
   private SecurityContextRepository repo;
  
   public SecurityContextPersistenceFilter() {
      //HttpSessionSecurityContextRepository是SecurityContextRepository介面的一個實現類
      //使用HttpSession來儲存SecurityContext
      this(new HttpSessionSecurityContextRepository());
   }

   public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
         throws IOException, ServletException {
      HttpServletRequest request = (HttpServletRequest) req;
      HttpServletResponse response = (HttpServletResponse) res;

      if (request.getAttribute(FILTER_APPLIED) != null) {
         // ensure that filter is only applied once per request
         chain.doFilter(request, response);
         return;
      }
      final boolean debug = logger.isDebugEnabled();
      request.setAttribute(FILTER_APPLIED, Boolean.TRUE);
      if (forceEagerSessionCreation) {
			HttpSession session = request.getSession();

			if (debug && session.isNew()) {
				logger.debug("Eagerly created session: " + session.getId());
			}
		}
      HttpRequestResponseHolder holder = new HttpRequestResponseHolder(request,
            response);
      //從Session中獲取安全上下文資訊
      SecurityContext contextBeforeChainExecution = repo.loadContext(holder);
      try {
         //請求開始時,設定安全上下文資訊,這樣就避免了使用者直接從Session中獲取安全上下文資訊
         SecurityContextHolder.setContext(contextBeforeChainExecution);
         chain.doFilter(holder.getRequest(), holder.getResponse());
      }
      finally {
         //請求結束後,清空安全上下文資訊
         SecurityContext contextAfterChainExecution = SecurityContextHolder
               .getContext();
         SecurityContextHolder.clearContext();
         repo.saveContext(contextAfterChainExecution, holder.getRequest(),
               holder.getResponse());
         request.removeAttribute(FILTER_APPLIED);
         if (debug) {
            logger.debug("SecurityContextHolder now cleared, as request processing completed");
         }
      }
   }

}

過濾器一般負責核心的處理流程,而具體的業務實現,通常交給其中聚合的其他實體類。例如儲存安全上下文和讀取安全上下文的工作完全委託給了HttpSessionSecurityContextRepository去處理:

public class HttpSessionSecurityContextRepository implements SecurityContextRepository {
   // 'SPRING_SECURITY_CONTEXT'是安全上下文預設儲存在Session中的鍵值
   public static final String SPRING_SECURITY_CONTEXT_KEY = "SPRING_SECURITY_CONTEXT";
   ...
   private final Object contextObject = SecurityContextHolder.createEmptyContext();
   private boolean allowSessionCreation = true;
   private boolean disableUrlRewriting = false;
   private String springSecurityContextKey = SPRING_SECURITY_CONTEXT_KEY;

   private AuthenticationTrustResolver trustResolver = new AuthenticationTrustResolverImpl();

   //從當前request中取出安全上下文,如果session為空,則會返回一個新的安全上下文
   public SecurityContext loadContext(HttpRequestResponseHolder requestResponseHolder) {
      HttpServletRequest request = requestResponseHolder.getRequest();
      HttpServletResponse response = requestResponseHolder.getResponse();
      HttpSession httpSession = request.getSession(false);
      SecurityContext context = readSecurityContextFromSession(httpSession);
      if (context == null) {
         context = generateNewContext();
      }
      ...
      return context;
   }

   ...

   public boolean containsContext(HttpServletRequest request) {
      HttpSession session = request.getSession(false);
      if (session == null) {
         return false;
      }
      return session.getAttribute(springSecurityContextKey) != null;
   }

   private SecurityContext readSecurityContextFromSession(HttpSession httpSession) {
      if (httpSession == null) {
         return null;
      }
      ...
      // Session存在的情況下,嘗試獲取其中的SecurityContext
      Object contextFromSession = httpSession.getAttribute(springSecurityContextKey);
      if (contextFromSession == null) {
         return null;
      }
      ...
      return (SecurityContext) contextFromSession;
   }

   //初次請求時建立一個新的SecurityContext例項
   protected SecurityContext generateNewContext() {
      return SecurityContextHolder.createEmptyContext();
   }

}

SecurityContextPersistenceFilter和HttpSessionSecurityContextRepository配合使用,構成了Spring Security整個呼叫鏈路的入口。

3.3 UsernamePasswordAuthenticationFilter

UsernamePasswordAuthenticationFilter用於處理來自表單提交的認證。

public class UsernamePasswordAuthenticationFilter extends
		AbstractAuthenticationProcessingFilter {
    //使用者名稱預設的引數名 可通過setUsernameParameter修改
	private String usernameParameter = SPRING_SECURITY_FORM_USERNAME_KEY;
    //密碼預設的引數名 可通過setPasswordParameter修改
	private String passwordParameter = SPRING_SECURITY_FORM_PASSWORD_KEY;
    //是否只允許post請求
	private boolean postOnly = true;

	public Authentication attemptAuthentication(HttpServletRequest request,
			HttpServletResponse response) throws AuthenticationException {
		if (postOnly && !request.getMethod().equals("POST")) {
			throw new AuthenticationServiceException(
					"Authentication method not supported: " + request.getMethod());
		}
        //獲取表單中的使用者名稱和密碼
		String username = obtainUsername(request);
		String password = obtainPassword(request);
		if (username == null) {
			username = "";
		}
		if (password == null) {
			password = "";
		}
		username = username.trim();
        //組裝成username+password形式的token
		UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
				username, password);
		// Allow subclasses to set the "details" property
		setDetails(request, authRequest);
        //交給內部的AuthenticationManager去認證,並返回認證後的資訊
		return this.getAuthenticationManager().authenticate(authRequest);
	}
}

UsernamePasswordAuthenticationFilter本身的程式碼只包含了上述這麼一個方法,非常簡略,而在其父類AbstractAuthenticationProcessingFilter中包含了大量的細節:

public abstract class AbstractAuthenticationProcessingFilter extends GenericFilterBean
      implements ApplicationEventPublisherAware, MessageSourceAware {
	//包含了一個身份認證器
	private AuthenticationManager authenticationManager;
	//用於實現remeberMe
	private RememberMeServices rememberMeServices = new NullRememberMeServices();
	private RequestMatcher requiresAuthenticationRequestMatcher;
	//這兩個Handler分別代表了認證成功和失敗相應的處理器
	private AuthenticationSuccessHandler successHandler = new SavedRequestAwareAuthenticationSuccessHandler();
	private AuthenticationFailureHandler failureHandler = new SimpleUrlAuthenticationFailureHandler();
	
	public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
			throws IOException, ServletException {

		HttpServletRequest request = (HttpServletRequest) req;
		HttpServletResponse response = (HttpServletResponse) res;
		...
		Authentication authResult;
		try {
			//此處實際上就是呼叫UsernamePasswordAuthenticationFilter的attemptAuthentication方法
			authResult = attemptAuthentication(request, response);
			if (authResult == null) {
				//子類未完成認證,立刻返回
				return;
			}
			sessionStrategy.onAuthentication(authResult, request, response);
		}
		//在認證過程中可以直接丟擲異常,在過濾器中,就像此處一樣,進行捕獲
		catch (InternalAuthenticationServiceException failed) {
			//內部服務異常
			unsuccessfulAuthentication(request, response, failed);
			return;
		}
		catch (AuthenticationException failed) {
			//認證失敗
			unsuccessfulAuthentication(request, response, failed);
			return;
		}
		//認證成功
		if (continueChainBeforeSuccessfulAuthentication) {
			chain.doFilter(request, response);
		}
		//注意,認證成功後過濾器把authResult結果也傳遞給了成功處理器
		successfulAuthentication(request, response, chain, authResult);
	}
	
}

整個流程主要就是呼叫了authenticationManager完成認證,根據認證結果執行successfulAuthentication或者unsuccessfulAuthentication,無論成功失敗,一般的實現都是轉發或者重定向等處理。

protected void successfulAuthentication(HttpServletRequest request,
			HttpServletResponse response, FilterChain chain, Authentication authResult)
			throws IOException, ServletException {
            ...
            successHandler.onAuthenticationSuccess(request, response, authResult);
	}

successHandler重定向到DefaultSavedRequest url

public class SavedRequestAwareAuthenticationSuccessHandler extends
		SimpleUrlAuthenticationSuccessHandler {
	@Override
	public void onAuthenticationSuccess(HttpServletRequest request,
			HttpServletResponse response, Authentication authentication)
			throws ServletException, IOException {
		SavedRequest savedRequest = requestCache.getRequest(request, response);
		if (savedRequest == null) {
			super.onAuthenticationSuccess(request, response, authentication);
			return;
		}
		String targetUrlParameter = getTargetUrlParameter();
		if (isAlwaysUseDefaultTargetUrl()
				|| (targetUrlParameter != null && StringUtils.hasText(request
						.getParameter(targetUrlParameter)))) {
			requestCache.removeRequest(request, response);
			super.onAuthenticationSuccess(request, response, authentication);

			return;
		}
        //清理認證資訊
		clearAuthenticationAttributes(request);

		// Use the DefaultSavedRequest URL
		String targetUrl = savedRequest.getRedirectUrl();
		logger.debug("Redirecting to DefaultSavedRequest Url: " + targetUrl);
		getRedirectStrategy().sendRedirect(request, response, targetUrl);
	}
}

而它的父類SimpleUrlAuthenticationSuccessHandler裡:

public class SimpleUrlAuthenticationSuccessHandler extends
		AbstractAuthenticationTargetUrlRequestHandler{
    public void onAuthenticationSuccess(HttpServletRequest request,
			HttpServletResponse response, Authentication authentication)
			throws IOException, ServletException {
		handle(request, response, authentication);
		clearAuthenticationAttributes(request);
	}
}

這個handle來自於AbstractAuthenticationTargetUrlRequestHandler:

public abstract class AbstractAuthenticationTargetUrlRequestHandler {
    protected void handle(HttpServletRequest request, HttpServletResponse response,
			Authentication authentication) throws IOException, ServletException {
		...
		redirectStrategy.sendRedirect(request, response, targetUrl);
	}
}

failureHandler結構和successHander類似,有興趣的可以研究一下。

在文章開頭我們指出,配置了http.formLogin()後會自動載入UsernamePasswordAuthenticationFilter,那麼是在什麼時候進行載入filter呢?在FormLoginConfigurer中找到了利用父類AbstractAuthenticationFilterConfigurer進行了對filter的配置:

public FormLoginConfigurer() {
		super(new UsernamePasswordAuthenticationFilter(), null);
		usernameParameter("username");
		passwordParameter("password");
}

而AbstractAuthenticationFilterConfigurer中:

public abstract class AbstractAuthenticationFilterConfigurer extends ...{
   ...
   //formLogin不出所料配置了AuthenticationEntryPoint
   private LoginUrlAuthenticationEntryPoint authenticationEntryPoint;
   //認證失敗的處理器
   private AuthenticationFailureHandler failureHandler;
   ...
   protected AbstractAuthenticationFilterConfigurer(F authenticationFilter,
			String defaultLoginProcessingUrl) {
		this();
		this.authFilter = authenticationFilter;
		if (defaultLoginProcessingUrl != null) {
			loginProcessingUrl(defaultLoginProcessingUrl);
		}
	}
}

也就是說,formLogin()配置了之後最起碼做了兩件事,其一,為UsernamePasswordAuthenticationFilter設定了相關的配置,其二配置了AuthenticationEntryPoint。AuthenticationEntryPoint在下面的章節詳細分析。

3.4 AnonymousAuthenticationFilter

匿名認證過濾器,可能有人會想:匿名了還有身份?Spirng Security為了整體邏輯的統一性,即使是未通過認證的使用者,也給予了一個匿名身份。而AnonymousAuthenticationFilter位於常用的身份認證過濾器(如UsernamePasswordAuthenticationFilter、RememberMeAuthenticationFilter)之後,意味著只有在上述身份過濾器執行完畢後,SecurityContext依舊沒有使用者資訊,AnonymousAuthenticationFilter會給予使用者一個匿名身份。

原始碼分析

public class AnonymousAuthenticationFilter extends GenericFilterBean implements
      InitializingBean {
   private AuthenticationDetailsSource<HttpServletRequest, ?> authenticationDetailsSource = new WebAuthenticationDetailsSource();
   private String key;
   private Object principal;
   private List<GrantedAuthority> authorities;
   //自動建立一個"anonymousUser"的匿名使用者,其具有ANONYMOUS角色
   public AnonymousAuthenticationFilter(String key) {
      this(key, "anonymousUser", AuthorityUtils.createAuthorityList("ROLE_ANONYMOUS"));
   }
   ...
   public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
         throws IOException, ServletException {
      //過濾器鏈都執行到匿名認證過濾器還沒有身份資訊,塞一個匿名身份進去
      if (SecurityContextHolder.getContext().getAuthentication() == null) {
         SecurityContextHolder.getContext().setAuthentication(
               createAuthentication((HttpServletRequest) req));
      }
      chain.doFilter(req, res);
   }

   protected Authentication createAuthentication(HttpServletRequest request) {
     //建立一個AnonymousAuthenticationToken
      AnonymousAuthenticationToken auth = new AnonymousAuthenticationToken(key,
            principal, authorities);
      auth.setDetails(authenticationDetailsSource.buildDetails(request));

      return auth;
   }
   ...
}

到這裡可以看出,AnonymousAuthenticationFilter和UsernamePasswordAuthenticationFilter都是對Authentication進行一系列操作,這就印證了前面說要建立一個全域性的SecurityContext,來把一系列的過濾器串聯起來。

3.5 ExceptionTranslationFilter

通過前面的介紹我們知道在Spring Security的Filter連結串列中ExceptionTranslationFilter就放在FilterSecurityInterceptor的前面。而ExceptionTranslationFilter是捕獲來自FilterChain的異常,並對這些異常做處理。ExceptionTranslationFilter能夠捕獲來自FilterChain所有的異常,但是它只會處理兩類異常,AuthenticationException和AccessDeniedException,其它的異常它會繼續丟擲。如果捕獲到的是AuthenticationException,那麼將會使用其對應的AuthenticationEntryPoint的commence()處理。如果捕獲的異常是一個AccessDeniedException,那麼將視當前訪問的使用者是否已經登入認證做不同的處理,如果未登入,則會使用關聯的AuthenticationEntryPoint的commence()方法進行處理,否則將使用關聯的AccessDeniedHandler的handle()方法進行處理。

原始碼分析

public class ExceptionTranslationFilter extends GenericFilterBean {
  //處理異常轉換的核心方法
  private void handleSpringSecurityException(HttpServletRequest request,
        HttpServletResponse response, FilterChain chain, RuntimeException exception)
        throws IOException, ServletException {
     if (exception instanceof AuthenticationException) {
       	//重定向到登入端點
        sendStartAuthentication(request, response, chain,
              (AuthenticationException) exception);
     }