對當前項目中使用到的Spring Security做一個簡單的理解總結,方便以后查閱。文章有疏漏之處,歡迎指正。
Spring Security是一個能夠為基於Spring的企業應用系統提供聲明式的安全訪 問控制解決方案的安全框架。它提供了一組可以在 Spring 應用上下文中配置的 Bean,充分利用了Spring IOC和AOP功能,為應用系統提供聲明式的安全訪問控制功能,減少了為企業系統安全控制編寫大量重復代碼的工作。應用程序層面的安全大概可以歸為兩類:身份認證和授權,Spring Security 在架構設計上就將兩者分開了,在每個架構上都留有擴展點。
1 身份認證架構
1.1 AuthenticationManager
身份認證的核心接口,只包含一個方法:
public interface AuthenticationManager { Authentication authenticate(Authentication authentication) throws AuthenticationException; }
authenticate 方法可能產生三種結果:
a) 如果身份認證成功,返回完備的 Authentication 對象(一般來說, authenticated=true)
b) 如果身份認證失敗,拋出 AuthenticationException
c) 如果無法認證,則返回 null
1.2 ProviderManager
是最常用的 AuthenticationManager 接口的實現類,它將工作委托給 AuthenticationProvider 鏈。AuthenticationProvider 的接口定義類似於 AuthenticationManager,只不過多了個方法讓調用者判斷其是否支持傳入的 Authentication 類型。
ProviderManager 會遍歷 AuthenticationProvider 鏈,先判 斷其是否支持傳入的 Authentication 類型,如果支持,則調用 authenticate 方法, 如返回不為 null 的 Authentication,則身份認證成功。

for (AuthenticationProvider provider : getProviders()) { if (!provider.supports(toTest)) { continue; } if (debug) { logger.debug("Authentication attempt using " + provider.getClass().getName()); } try { result = provider.authenticate(authentication); if (result != null) { copyDetails(authentication, result); break; } } catch (AccountStatusException e) { prepareException(e, authentication); // SEC-546: Avoid polling additional providers if auth failure is due to // invalid account status throw e; } catch (InternalAuthenticationServiceException e) { prepareException(e, authentication); throw e; } catch (AuthenticationException e) { lastException = e; } }
2 授權架構
2.1 AccessDecisionManager
授權的核心接口,包含三個方法:
public interface AccessDecisionManager { void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes) throws AccessDeniedException, InsufficientAuthenticationException; boolean supports(ConfigAttribute attribute); boolean supports(Class<?> clazz); }
decide 方法決定是否允許訪問被保護的對象,傳入的參數中,authentication 是被身份認證通過后的完備對象,object是被保護的對象, configAttributes是被保護對象的屬性信息。
2.2 AbstractAccessDecisionManager
實現了 AccessDecisionManager 接口的抽象類,它管理了一個 AccessDecisionVoter 列表,在執行授權操作時,調用每個 AccessDecisionVoter 的 vote 方法得到單獨投票結果,對每個 voter 投票結果的仲裁則由其子類來完成。
Spring Security 提供了 3 個子類,每個子類的仲裁邏輯如下:
AffirmativeBased:只要有 voter投了贊成票,則授權成功;在沒有贊成票的情況下,只要有反對票,則授權失敗;在全部棄權的情況下,根據 isAllowIfAllAbstainDecisions 方法的返回值決定是否授權。
ConsensusBased: 根據少數服從多數的原則決定是否授權,如果贊成票和反對票相等,根據 isAllowIfAllAbstainDecisions 方法的返回值決定是否授權。
UnanimousBased:只有全部是贊成票或者棄權才授權成功,只要有反對票則授權失敗。如果全部棄權,根據 isAllowIfAllAbstainDecisions 方法的返回值決定是否授權。
2.3 AccessDecisionVoter
對是否授權進行投票,核心方法是 vote, 該方法只能返回 3 種 int 值: ACCESS_GRANTED(1),ACCESS_ABSTAIN(0),ACCESS_DENIED(-1)。
這兒就是一個擴展點,項目可以實現自己的voter,例如: SimpleDecisionVoter implements AccessDecisionVoter<FilterInvocation>
3 Web 層安全
Spring Security 在 web 層的應用是基於 Servlet Filter 實現的,具體的實現類是 DelegatingFilterProxy,該代理類又會委托 Spring 容器管理的 FilterChainProxy 來處理,
FilterChainProxy 維護了一系列內部的 filter, 正是這些內部的 filter 實現 了全部的安全邏輯。關系圖如下:
在web.xml在配置即可:
<filter> <filter-name>springSecurityFilterChain</filter-name> <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class> </filter> <filter-mapping> <filter-name>springSecurityFilterChain</filter-name> <url-pattern>/*</url-pattern> </filter-mapping>
3.1 核心過濾器
以我所在的項目組對Spring Security的使用為例,一次web請求的filter chain 如下:
3.1.1 SecurityContextPersistenceFilter
此filter先嘗試從servlet session中獲取 SecurityContext ,如果沒有獲取到,則創建一個空的 SecurityContext 對象,SecurityContextHolder 是 SecurityContext 的存放容器,使用 ThreadLocal 存儲並將此對象跟當前線程關聯。
下圖顯示了整個交互過程:
3.1.2 LogoutFilter
先判斷請求的 url 是否匹配 logout-url,如果匹配則重定向到指定的 url,否則直接調用下一個 filter。下圖顯示了整個交互過程:
屬性名 | 作用 |
logout-url | 表示此請求做為退出登錄的默認地址 |
invalidate-session | 表示是否要在退出登錄后讓當前session失效,默認為true。 |
delete-cookies | 指定退出登錄后需要刪除的cookie名稱,多個cookie之間以逗號分隔。 |
logout-success-url | 指定成功退出登錄后要重定向的URL。需要注意的是對應的URL應當是不需要登錄就可以訪問的。 |
success-handler-ref | 指定用來處理成功退出登錄的LogoutSuccessHandler的引用。 |
簡單看一下LogoutFilter的源碼
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) req; HttpServletResponse response = (HttpServletResponse) res; if (requiresLogout(request, response)) { Authentication auth = SecurityContextHolder.getContext().getAuthentication(); if (logger.isDebugEnabled()) { logger.debug("Logging out user '" + auth + "' and transferring to logout destination"); } this.handler.logout(request, response, auth); logoutSuccessHandler.onLogoutSuccess(request, response, auth); return; } chain.doFilter(request, response); }
如果 requiresLogout(request, response)為true,則分別調用 CompositeLogoutHandler 的 logout(request, response, auth) 方法和 LogoutSuccessHandler 的 onLogoutSuccess(request, response, auth);
在這兩個方法中,logout 和 onLogoutSuccess 除了執行Spring Security自己的一些內部方法,比如 SecurityContextLogoutHandler 的 logout,我們也可以自己定義自己的方法,退出登錄需要刪除自定義的cookie等。
3.1.3 UsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter
先看看 AbstractAuthenticationProcessingFilter 的 doFilter 方法
1 public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) 2 throws IOException, ServletException { 3 4 HttpServletRequest request = (HttpServletRequest) req; 5 HttpServletResponse response = (HttpServletResponse) res; 6 7 if (!requiresAuthentication(request, response)) { 8 chain.doFilter(request, response); 9 10 return; 11 } 12 13 if (logger.isDebugEnabled()) { 14 logger.debug("Request is to process authentication"); 15 } 16 17 Authentication authResult; 18 19 try { 20 authResult = attemptAuthentication(request, response); 21 if (authResult == null) { 22 // return immediately as subclass has indicated that it hasn't completed 23 // authentication 24 return; 25 } 26 sessionStrategy.onAuthentication(authResult, request, response); 27 } 28 catch (InternalAuthenticationServiceException failed) { 29 logger.error( 30 "An internal error occurred while trying to authenticate the user.", 31 failed); 32 unsuccessfulAuthentication(request, response, failed); 33 34 return; 35 } 36 catch (AuthenticationException failed) { 37 // Authentication failed 38 unsuccessfulAuthentication(request, response, failed); 39 40 return; 41 } 42 43 // Authentication success 44 if (continueChainBeforeSuccessfulAuthentication) { 45 chain.doFilter(request, response); 46 } 47 48 successfulAuthentication(request, response, chain, authResult); 49 }
20行的 attemptAuthentication(request, response) 就是 UsernamePasswordAuthenticationFilter 是嘗試認證過程。
--------------------------------------UsernamePasswordAuthenticationFilter 分析開始---------------------------------------------------------------
身份認證過程:根據 form 表單中的用戶名獲取用戶信息(包含密碼),將數 據庫中的密碼和 form 表單中的密碼做比對,若匹配則身份認證成功,並調用 AuthenticationSuccessHandler. onAuthenticationSuccess()。
下圖顯示的是認證成 功的交互過程:
因為真實項目登錄除了校驗用戶名和密碼外,可能還有有些額外的校驗,比如驗證碼之類的,所有我們會自定義一個類去繼承 UsernamePasswordAuthenticationFilter,然后重寫 attemptAuthentication() 方法。
類 UsernamePasswordAuthenticationFilter 的 attemptAuthentication(HttpServletRequest request, HttpServletResponse response)方法:

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(); UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken( username, password); // Allow subclasses to set the "details" property setDetails(request, authRequest); return this.getAuthenticationManager().authenticate(authRequest); }
然后執行類 ProviderManager 的 authenticate(Authentication authentication) 方法,已經到了身份認證架構部分,
繼續調用抽象類 AbstractUserDetailsAuthenticationProvider 的 authenticate(Authentication authentication) 方法:

1 public Authentication authenticate(Authentication authentication) 2 throws AuthenticationException { 3 Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication, 4 messages.getMessage( 5 "AbstractUserDetailsAuthenticationProvider.onlySupports", 6 "Only UsernamePasswordAuthenticationToken is supported")); 7 8 // Determine username 9 String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED" 10 : authentication.getName(); 11 12 boolean cacheWasUsed = true; 13 UserDetails user = this.userCache.getUserFromCache(username); 14 15 if (user == null) { 16 cacheWasUsed = false; 17 18 try { 19 user = retrieveUser(username, 20 (UsernamePasswordAuthenticationToken) authentication); 21 } 22 catch (UsernameNotFoundException notFound) { 23 logger.debug("User '" + username + "' not found"); 24 25 if (hideUserNotFoundExceptions) { 26 throw new BadCredentialsException(messages.getMessage( 27 "AbstractUserDetailsAuthenticationProvider.badCredentials", 28 "Bad credentials")); 29 } 30 else { 31 throw notFound; 32 } 33 } 34 35 Assert.notNull(user, 36 "retrieveUser returned null - a violation of the interface contract"); 37 } 38 39 try { 40 preAuthenticationChecks.check(user); 41 additionalAuthenticationChecks(user, 42 (UsernamePasswordAuthenticationToken) authentication); 43 } 44 catch (AuthenticationException exception) { 45 if (cacheWasUsed) { 46 // There was a problem, so try again after checking 47 // we're using latest data (i.e. not from the cache) 48 cacheWasUsed = false; 49 user = retrieveUser(username, 50 (UsernamePasswordAuthenticationToken) authentication); 51 preAuthenticationChecks.check(user); 52 additionalAuthenticationChecks(user, 53 (UsernamePasswordAuthenticationToken) authentication); 54 } 55 else { 56 throw exception; 57 } 58 } 59 60 postAuthenticationChecks.check(user); 61 62 if (!cacheWasUsed) { 63 this.userCache.putUserInCache(user); 64 } 65 66 Object principalToReturn = user; 67 68 if (forcePrincipalAsString) { 69 principalToReturn = user.getUsername(); 70 } 71 72 return createSuccessAuthentication(principalToReturn, authentication, user); 73 }
19行 retrieveUser 方法的具體實現在類 DaoAuthenticationProvider 中,這兒就是從我們系統中根據 username,查詢到一個 UserDetails 數據。
40行 preAuthenticationChecks.check(user); 則是為已經查詢到的用戶,附上在當前系統中的權限數據。
41行 additionalAuthenticationChecks 里面就是密碼的校驗,具體實現類還是 DaoAuthenticationProvider,自定義的 passwordEncoderHandler 實現了 PasswordEncoder 接口 isPasswordValid 方法,去比較密碼是否匹配。
--------------------------------------UsernamePasswordAuthenticationFilter 分析結束---------------------------------------------------------------
繼續 AbstractAuthenticationProcessingFilter 的 doFilter 方法分析,
32和38行 unsuccessfulAuthentication(request, response, failed) 是認證失敗的處理邏輯,
認證失敗時,首先 SecurityContextHolder.clearContext(),清除認證信息,然后在 SimpleUrlAuthenticationFailureHandler 可以處理自定義的認證失敗邏輯
48行的 successfulAuthentication(request, response, chain, authResult) 則是認證成功的邏輯。
認證成功時,首先 SecurityContextHolder.getContext().setAuthentication(authentication),其次執行 AbstractRememberMeServices 的 loginSuccess 方法,然后執行 SavedRequestAwareAuthenticationSuccessHandler的 onAuthenticationSuccess方法。
RememberMeAuthenticationFilter
這個filter貌似是查詢 當前SecurityContextHolder的Authentication是否為null,如果為空則繼續走類似上一個filter的身份認證過程,如果不為空,繼續下一個filter。
3.1.4 RequestCacheAwareFilter
此 filter 通常用作臨時重定向,場景是這樣的:在未登陸的情況下訪問某個安全 url,會重定向到登陸頁,在重定向之前先在 session 中保存訪問此安全 url 的請求,在登陸成功后再繼續處理它。下圖顯示的是交互過程:
圖中 SAVED_REQUEST = "SPRING_SECURITY_SAVED_REQUEST"
3.1.5 SecurityContextHolderAwareRequestFilter
將 servlet request 封裝到 SecurityContextHolderAwareRequestWrapper 類中,此 包裝類繼承自 HttpServletRequestWrapper,並提供了額外的跟安全相關的方法。
3.1.6 AnonymousAuthenticationFilter
如果到了這步 SecurityContext 中的 Authentication 對象仍然為null, 則創建一個 AnonymousAuthenticationToken,這個對象可以類比網站的匿名用戶。下圖展示 的是交互過程:
3.1.7 SessionManagementFilter
這個過濾器看名字就知道是管理 session 的了,提供兩大類功能: session 固化 保護(通過 session-fixation-protection 配置),session 並發控制(通過 concurrency-control 配置)。
3.1.8 ExceptionTranslationFilter
當請求到達這里時,會將對后續過濾器的調用封裝在 try..catch 塊中,如果捕獲 到 AuthenticationException 異常,則調用 authenticationEntryPoint.commence 方法開啟新的身份認證流程(一般來說是跳轉到登陸頁),
如果捕獲到 AccessDeniedException 異常,調用 accessDeniedHandler.handle 方法處理(比如: 展示未授權的錯誤頁面)。下圖顯示的是交互過程:
3.1.9 FilterSecurityInterceptor
此過濾器是整個過濾器鏈的最后一環,用於保護 Http 資源的,它需要一個 AccessDecisionManager 和一個 AuthenticationManager 的引用。它會從 SecurityContextHolder 獲取 Authentication,
然后通過 SecurityMetadataSource 可以得知當前請求是否在請求受保護的資源。對於請求那些受保護的資源,如果 Authentication.isAuthenticated()返回 false 或者 FilterSecurityInterceptor 的 alwaysReauthenticate 屬性為 true ,
那么將會使用其引用的 AuthenticationManager 再認證一次,認證之后再使用認證后的 Authentication替換 SecurityContextHolder 中擁有的那個。然后就是利用 AccessDecisionManager 進行權限的檢查,也就進行到了授權架構部分。
下圖顯示的是交互過程:
3.2其他過濾器
3.2.1 CsrfFilter
該過濾器通過 token 防范 CSRF 攻擊,將從 tokenRepository 獲取的 token 與從 請求參數或者 header 參數中獲取的 token 做比較,如果不匹配,調用 accessDeniedHandler.handle 方法來處理。下圖展示的是交互過程:
3.2.2 HeaderWriterFilter
該過濾器通過寫響應安全頭的方式來保護瀏覽器端的安全,下面簡單介紹下各種 http 安全頭。
content-security-policy:通過定義內容來源來預防 XSS 攻擊或者代碼植入攻擊,下面 的例子只允許當前域或者 google-analytics.com 的腳本執行。
X-XSS-Protection: 又稱 XSS 過濾器,通過指示瀏覽器阻止含有惡意腳本的響應來預防 XSS 攻擊。
strict-transport-security(HSTS):該頭指示瀏覽器只能使用 https 訪問 web server。
X-Frame-Options:來確保自己網站的內容沒有被嵌到別人的網站中去,也從而避免了點擊 劫持 (clickjacking) 的攻擊。X-Frame-Options 有三個值: DENY(表示該頁面不允許 在 frame 中展示,即便是在相同域名的頁面中嵌套也不允許),
SAMEORIGIN(表示該頁面可 以在相同域名頁面的 frame 中展示),ALLOW-FROM uri(表示該頁面可以在指定來源的 frame 中展示)。
X-Content-Type-Options:如果服務器發送響應頭 "X-Content-Type-Options: nosniff",則 script 和 styleSheet 元素會拒絕包含錯誤的 MIME 類型的響應。這是一 種安全功能,有助於防止基於 MIME 類型混淆的攻擊。