對當前項目中使用到的Spring Security作一個簡單的理解總結,方便之後查閱。文章有疏漏之處,歡迎指正。web
Spring Security是一個可以爲基於Spring的企業應用系統提供聲明式的安全訪 問控制解決方案的安全框架。它提供了一組能夠在 Spring 應用上下文中配置的 Bean,充分利用了Spring IOC和AOP功能,爲應用系統提供聲明式的安全訪問控制功能,減小了爲企業系統安全控制編寫大量重複代碼的工做。應用程序層面的安全大概能夠歸爲兩類:身份認證和受權,Spring Security 在架構設計上就將二者分開了,在每一個架構上都留有擴展點。spring
身份認證的核心接口,只包含一個方法:
public interface AuthenticationManager { Authentication authenticate(Authentication authentication) throws AuthenticationException; }
authenticate 方法可能產生三種結果:
a) 若是身份認證成功,返回完備的 Authentication 對象(通常來講, authenticated=true)
b) 若是身份認證失敗,拋出 AuthenticationException
c) 若是沒法認證,則返回 null 瀏覽器
是最經常使用的 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; } }
受權的核心接口,包含三個方法:
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是被保護對象的屬性信息。cookie
實現了 AccessDecisionManager 接口的抽象類,它管理了一個 AccessDecisionVoter 列表,在執行受權操做時,調用每一個 AccessDecisionVoter 的 vote 方法獲得單獨投票結果,對每一個 voter 投票結果的仲裁則由其子類來完成。session
Spring Security 提供了 3 個子類,每一個子類的仲裁邏輯以下:架構
AffirmativeBased:只要有 voter投了同意票,則受權成功;在沒有同意票的狀況下,只要有反對票,則受權失敗;在所有棄權的狀況下,根據 isAllowIfAllAbstainDecisions 方法的返回值決定是否受權。併發
ConsensusBased: 根據少數服從多數的原則決定是否受權,若是同意票和反對票相等,根據 isAllowIfAllAbstainDecisions 方法的返回值決定是否受權。app
UnanimousBased:只有所有是同意票或者棄權才受權成功,只要有反對票則受權失敗。若是所有棄權,根據 isAllowIfAllAbstainDecisions 方法的返回值決定是否受權。
對是否受權進行投票,核心方法是 vote, 該方法只能返回 3 種 int 值: ACCESS_GRANTED(1),ACCESS_ABSTAIN(0),ACCESS_DENIED(-1)。
這兒就是一個擴展點,項目能夠實現本身的voter,例如: SimpleDecisionVoter implements AccessDecisionVoter<FilterInvocation>
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>
以我所在的項目組對Spring Security的使用爲例,一次web請求的filter chain 以下:
此filter先嚐試從servlet session中獲取 SecurityContext ,若是沒有獲取到,則建立一個空的 SecurityContext 對象,SecurityContextHolder 是 SecurityContext 的存放容器,使用 ThreadLocal 存儲並將此對象跟當前線程關聯。
下圖顯示了整個交互過程:
先判斷請求的 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等。
先看看 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方法。
這個filter貌似是查詢 當前SecurityContextHolder的Authentication是否爲null,若是爲空則繼續走相似上一個filter的身份認證過程,若是不爲空,繼續下一個filter。
此 filter 一般用做臨時重定向,場景是這樣的:在未登錄的狀況下訪問某個安全 url,會重定向到登錄頁,在重定向以前先在 session 中保存訪問此安全 url 的請求,在登錄成功後再繼續處理它。下圖顯示的是交互過程:
圖中 SAVED_REQUEST = "SPRING_SECURITY_SAVED_REQUEST"
將 servlet request 封裝到 SecurityContextHolderAwareRequestWrapper 類中,此 包裝類繼承自 HttpServletRequestWrapper,並提供了額外的跟安全相關的方法。
若是到了這步 SecurityContext 中的 Authentication 對象仍然爲null, 則建立一個 AnonymousAuthenticationToken,這個對象能夠類比網站的匿名用戶。下圖展現 的是交互過程:
這個過濾器看名字就知道是管理 session 的了,提供兩大類功能: session 固化 保護(經過 session-fixation-protection 配置),session 併發控制(經過 concurrency-control 配置)。
當請求到達這裏時,會將對後續過濾器的調用封裝在 try..catch 塊中,若是捕獲 到 AuthenticationException 異常,則調用 authenticationEntryPoint.commence 方法開啓新的身份認證流程(通常來講是跳轉到登錄頁),
若是捕獲到 AccessDeniedException 異常,調用 accessDeniedHandler.handle 方法處理(好比: 展現未受權的錯誤頁面)。下圖顯示的是交互過程:
此過濾器是整個過濾器鏈的最後一環,用於保護 Http 資源的,它須要一個 AccessDecisionManager 和一個 AuthenticationManager 的引用。它會從 SecurityContextHolder 獲取 Authentication,
而後經過 SecurityMetadataSource 能夠得知當前請求是否在請求受保護的資源。對於請求那些受保護的資源,若是 Authentication.isAuthenticated()返回 false 或者 FilterSecurityInterceptor 的 alwaysReauthenticate 屬性爲 true ,
那麼將會使用其引用的 AuthenticationManager 再認證一次,認證以後再使用認證後的 Authentication替換 SecurityContextHolder 中擁有的那個。而後就是利用 AccessDecisionManager 進行權限的檢查,也就進行到了受權架構部分。
下圖顯示的是交互過程:
該過濾器經過 token 防範 CSRF 攻擊,將從 tokenRepository 獲取的 token 與從 請求參數或者 header 參數中獲取的 token 作比較,若是不匹配,調用 accessDeniedHandler.handle 方法來處理。下圖展現的是交互過程:
該過濾器經過寫響應安全頭的方式來保護瀏覽器端的安全,下面簡單介紹下各類 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 類型混淆的攻擊。