咱們已經知道Spring Security使用了springSecurityFillterChian做爲了安全過濾的入口,這裏主要分析一下這個過濾器鏈都包含了哪些關鍵的過濾器,而且各自的使命是什麼。java
因爲過濾器鏈路中的過濾較多,即便是Spring Security的官方文檔中也並未對全部的過濾器進行介紹,在以前,《Spring Security(二)—Guides》入門指南中咱們配置了一個表單登陸的demo,以此爲例,來看看這過程當中Spring Security都幫咱們自動配置了哪些過濾器。web
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 ]
上述的log信息是我從springboot啓動的日誌中CV所得,spring security的過濾器日誌有一個特色:log打印順序與實際配置順序符合,也就意味着SecurityContextPersistenceFilter
是整個過濾器鏈的第一個過濾器,而FilterSecurityInterceptor
則是末置的過濾器。另外經過觀察過濾器的名稱,和所在的包名,能夠大體地分析出他們各自的做用,如UsernamePasswordAuthenticationFilter
明顯即是與使用用戶名和密碼登陸相關的過濾器,而FilterSecurityInterceptor
咱們彷佛看不出它的做用,可是其位於web.access
包下,大體能夠分析出他與訪問限制相關。第四篇文章主要就是介紹這些經常使用的過濾器,對其中關鍵的過濾器進行一些源碼分析。先大體介紹下每一個過濾器的做用:spring
SecurityContext
安全上下文信息,請求結束時清空SecurityContextHolder
。session-fixation protection attack
,以及限制同一用戶開啓多個會話的數量其中加粗的過濾器能夠被認爲是Spring Security的核心過濾器,將在下面,一個過濾器對應一個小節來說解。json
試想一下,若是咱們不使用Spring Security,若是保存用戶信息呢,大多數狀況下會考慮使用Session對吧?在Spring Security中也是如此,用戶在登陸過一次以後,後續的訪問即是經過sessionId來識別,從而認爲用戶已經被認證。具體在何處存放用戶信息,即是第一篇文章中提到的SecurityContextHolder;認證相關的信息是如何被存放到其中的,即是經過SecurityContextPersistenceFilter。在4.1概述中也提到了,SecurityContextPersistenceFilter的兩個主要做用即是請求來臨時,建立SecurityContext
安全上下文信息和請求結束時清空SecurityContextHolder
。順帶提一下:微服務的一個設計理念須要實現服務通訊的無狀態,而http協議中的無狀態意味着不容許存在session,這能夠經過setAllowSessionCreation(false)
實現,這並不意味着SecurityContextPersistenceFilter變得無用,由於它還須要負責清除用戶信息。在Spring Security中,雖然安全上下文信息被存儲於Session中,但咱們在實際使用中不該該直接操做Session,而應當使用SecurityContextHolder。後端
org.springframework.security.web.context.SecurityContextPersistenceFilter
api
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; } request.setAttribute(FILTER_APPLIED, Boolean.TRUE); //包裝request,response 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"); } } } }
過濾器通常負責核心的處理流程,而具體的業務實現,一般交給其中聚合的其餘實體類,這在Filter的設計中很常見,同時也符合職責分離模式。例如存儲安全上下文和讀取安全上下文的工做徹底委託給了HttpSessionSecurityContextRepository去處理,而這個類中也有幾個方法能夠稍微解讀下,方便咱們理解內部的工做流程緩存
org.springframework.security.web.context.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整個調用鏈路的入口,爲何將它放在最開始的地方也是顯而易見的,後續的過濾器中大機率會依賴Session信息和安全上下文信息。springboot
表單認證是最經常使用的一個認證方式,一個最直觀的業務場景即是容許用戶在表單中輸入用戶名和密碼進行登陸,而這背後的UsernamePasswordAuthenticationFilter,在整個Spring Security的認證體系中則扮演着相當重要的角色。session
上述的時序圖,能夠看出UsernamePasswordAuthenticationFilter主要肩負起了調用身份認證器,校驗身份的做用,至於認證的細節,在前面幾章花了很大篇幅進行了介紹,到這裏,其實Spring Security的基本流程就已經走通了。
org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter#attemptAuthentication
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { //獲取表單中的用戶名和密碼 String username = obtainUsername(request); String password = obtainPassword(request); ... 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,不管成功失敗,通常的實現都是轉發或者重定向等處理,再也不細究AuthenticationSuccessHandler和AuthenticationFailureHandler,有興趣的朋友,能夠去看看二者的實現類。
匿名認證過濾器,可能有人會想:匿名了還有身份?我本身對於Anonymous匿名身份的理解是Spirng Security爲了總體邏輯的統一性,即便是未經過認證的用戶,也給予了一個匿名身份。而AnonymousAuthenticationFilter
該過濾器的位置也是很是的科學的,它位於經常使用的身份認證過濾器(如UsernamePasswordAuthenticationFilter
、BasicAuthenticationFilter
、RememberMeAuthenticationFilter
)以後,意味着只有在上述身份過濾器執行完畢後,SecurityContext依舊沒有用戶信息,AnonymousAuthenticationFilter
該過濾器纔會有意義——基於用戶一個匿名身份。
org.springframework.security.web.authentication.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")); } /** * * @param key key用來識別該過濾器建立的身份 * @param principal principal表明匿名用戶的身份 * @param authorities authorities表明匿名用戶的權限集合 */ public AnonymousAuthenticationFilter(String key, Object principal, List<GrantedAuthority> authorities) { Assert.hasLength(key, "key cannot be null or empty"); Assert.notNull(principal, "Anonymous authentication principal must be set"); Assert.notNull(authorities, "Anonymous authorities must be set"); this.key = key; this.principal = principal; this.authorities = authorities; } ... 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就能夠發現一些門道了,UsernamePasswordAuthenticationToken對應AnonymousAuthenticationToken,他們都是Authentication的實現類,而Authentication則是被SecurityContextHolder(SecurityContext)持有的,一切都被串聯在了一塊兒。
ExceptionTranslationFilter異常轉換過濾器位於整個springSecurityFilterChain的後方,用來轉換整個鏈路中出現的異常,將其轉化,顧名思義,轉化以意味自己並不處理。通常其只處理兩大類異常:AccessDeniedException訪問異常和AuthenticationException認證異常。
這個過濾器很是重要,由於它將Java中的異常和HTTP的響應鏈接在了一塊兒,這樣在處理異常時,咱們不用考慮密碼錯誤該跳到什麼頁面,帳號鎖定該如何,只須要關注本身的業務邏輯,拋出相應的異常即可。若是該過濾器檢測到AuthenticationException,則將會交給內部的AuthenticationEntryPoint去處理,若是檢測到AccessDeniedException,須要先判斷當前用戶是否是匿名用戶,若是是匿名訪問,則和前面同樣運行AuthenticationEntryPoint,不然會委託給AccessDeniedHandler去處理,而AccessDeniedHandler的默認實現,是AccessDeniedHandlerImpl。因此ExceptionTranslationFilter內部的AuthenticationEntryPoint是相當重要的,顧名思義:認證的入口點。
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); } else if (exception instanceof AccessDeniedException) { Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); if (authenticationTrustResolver.isAnonymous(authentication) || authenticationTrustResolver.isRememberMe(authentication)) { //重定向到登陸端點 sendStartAuthentication( request, response, chain, new InsufficientAuthenticationException( "Full authentication is required to access this resource")); } else { //交給accessDeniedHandler處理 accessDeniedHandler.handle(request, response, (AccessDeniedException) exception); } } } }
剩下的即是要搞懂AuthenticationEntryPoint和AccessDeniedHandler就能夠了。
選擇了幾個經常使用的登陸端點,以其中第一個爲例來介紹,看名字就能猜到是認證失敗以後,讓用戶跳轉到登陸頁面。還記得咱們一開始怎麼配置表單登陸頁面的嗎?
@Configuration @EnableWebSecurity public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http .authorizeRequests() .antMatchers("/", "/home").permitAll() .anyRequest().authenticated() .and() .formLogin()//FormLoginConfigurer .loginPage("/login") .permitAll() .and() .logout() .permitAll(); } }
咱們順着formLogin返回的FormLoginConfigurer往下找,看看能發現什麼,最終在FormLoginConfigurer的父類AbstractAuthenticationFilterConfigurer中有了不小的收穫:
public abstract class AbstractAuthenticationFilterConfigurer extends ...{ ... //formLogin不出所料配置了AuthenticationEntryPoint private LoginUrlAuthenticationEntryPoint authenticationEntryPoint; //認證失敗的處理器 private AuthenticationFailureHandler failureHandler; ... }
具體如何配置的就不看了,咱們得出告終論,formLogin()配置了以後最起碼作了兩件事,其一,爲UsernamePasswordAuthenticationFilter設置了相關的配置,其二配置了AuthenticationEntryPoint。
登陸端點還有Http401AuthenticationEntryPoint,Http403ForbiddenEntryPoint這些都是很簡單的實現,有時候咱們訪問受限頁面,又沒有配置登陸,就看到了一個空蕩蕩的默認錯誤頁面,上面顯示着401,403,就是這兩個入口起了做用。
還剩下一個AccessDeniedHandler訪問決策器未被講解,簡單提一下:AccessDeniedHandlerImpl這個默認實現類會根據errorPage和狀態碼來判斷,最終決定跳轉的頁面
org.springframework.security.web.access.AccessDeniedHandlerImpl#handle
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException { if (!response.isCommitted()) { if (errorPage != null) { // Put exception into request scope (perhaps of use to a view) request.setAttribute(WebAttributes.ACCESS_DENIED_403, accessDeniedException); // Set the 403 status code. response.setStatus(HttpServletResponse.SC_FORBIDDEN); // forward to error page. RequestDispatcher dispatcher = request.getRequestDispatcher(errorPage); dispatcher.forward(request, response); } else { response.sendError(HttpServletResponse.SC_FORBIDDEN, accessDeniedException.getMessage()); } } }
想一想整個認證安全控制流程還缺了什麼?咱們已經有了認證,有了請求的封裝,有了Session的關聯…還缺一個:由什麼控制哪些資源是受限的,這些受限的資源須要什麼權限,須要什麼角色…這一切和訪問控制相關的操做,都是由FilterSecurityInterceptor完成的。
FilterSecurityInterceptor的工做流程用筆者的理解能夠理解以下:FilterSecurityInterceptor從SecurityContextHolder中獲取Authentication對象,而後比對用戶擁有的權限和資源所需的權限。前者能夠經過Authentication對象直接得到,然後者則須要引入咱們以前一直未提到過的兩個類:SecurityMetadataSource,AccessDecisionManager。理解清楚決策管理器的整個建立流程和SecurityMetadataSource的做用須要花很大一筆功夫,這裏,暫時只介紹其大概的做用。
在JavaConfig的配置中,咱們一般以下配置路徑的訪問控制:
@Override protected void configure(HttpSecurity http) throws Exception { http .authorizeRequests() .antMatchers("/resources/**", "/signup", "/about").permitAll() .antMatchers("/admin/**").hasRole("ADMIN") .antMatchers("/db/**").access("hasRole('ADMIN') and hasRole('DBA')") .anyRequest().authenticated() .withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() { public <O extends FilterSecurityInterceptor> O postProcess( O fsi) { fsi.setPublishAuthorizationSuccess(true); return fsi; } }); }
在ObjectPostProcessor的泛型中看到了FilterSecurityInterceptor,以筆者的經驗,目前並無太多機會須要修改FilterSecurityInterceptor的配置。