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);
}