知彼知己方能百戰百勝,用 Spring Security 來知足咱們的需求最好了解其原理,這樣才能隨意拓展,本篇文章主要記錄 Spring Security 的基本運行流程。
Spring Security 基本都是經過過濾器來完成配置的身份認證、權限認證以及登出。前端
Spring Security 在 Servlet 的過濾鏈(filter chain)中註冊了一個過濾器 FilterChainProxy
,它會把請求代理到 Spring Security 本身維護的多個過濾鏈,每一個過濾鏈會匹配一些 URL,若是匹配則執行對應的過濾器。過濾鏈是有順序的,一個請求只會執行第一條匹配的過濾鏈。Spring Security 的配置本質上就是新增、刪除、修改過濾器。緩存
默認狀況下系統幫咱們注入的這 15 個過濾器,分別對應配置不一樣的需求。接下來咱們重點是分析下 UsernamePasswordAuthenticationFilter
這個過濾器,他是用來使用用戶名和密碼登陸認證的過濾器,可是不少狀況下咱們的登陸不止是簡單的用戶名和密碼,又多是用到第三方受權登陸,這個時候咱們就須要使用自定義過濾器,固然這裏不作詳細說明,只是說下自定義過濾器怎麼注入。session
@Override protected void configure(HttpSecurity http) throws Exception { http.addFilterAfter(...); ... }
在開始身份認證流程以前咱們須要瞭解下幾個基本概念app
SecurityContextHolder
存儲 SecurityContext
對象。SecurityContextHolder
是一個存儲代理,有三種存儲模式分別是:ide
SecurityContext
存儲在線程中,但子線程能夠獲取到父線程中的 SecurityContext
。SecurityContext
在全部線程中都相同。SecurityContextHolder
默認使用 MODE_THREADLOCAL 模式,SecurityContext
存儲在當前線程中。調用 SecurityContextHolder
時不須要顯示的參數傳遞,在當前線程中能夠直接獲取到 SecurityContextHolder
對象。post
//獲取當前線程裏面認證的對象 SecurityContext context = SecurityContextHolder.getContext(); Authentication authentication = context.getAuthentication(); //保存認證對象 (通常用於自定義認證成功保存認證對象) SecurityContextHolder.getContext().setAuthentication(authResult); //清空認證對象 (通常用於自定義登出清空認證對象) SecurityContextHolder.clearContext();
Authentication
即驗證,代表當前用戶是誰。什麼是驗證,好比一組用戶名和密碼就是驗證,固然錯誤的用戶名和密碼也是驗證,只不過 Spring Security 會校驗失敗。ui
Authentication
接口this
public interface Authentication extends Principal, Serializable { //獲取用戶權限,通常狀況下獲取到的是用戶的角色信息 Collection<? extends GrantedAuthority> getAuthorities(); //獲取證實用戶認證的信息,一般狀況下獲取到的是密碼等信息,不過登陸成功就會被移除 Object getCredentials(); //獲取用戶的額外信息,好比 IP 地址、經緯度等 Object getDetails(); //獲取用戶身份信息,在未認證的狀況下獲取到的是用戶名,在已認證的狀況下獲取到的是 UserDetails (暫時理解爲,當前應用用戶對象的擴展) Object getPrincipal(); //獲取當前 Authentication 是否已認證 boolean isAuthenticated(); //設置當前 Authentication 是否已認證 void setAuthenticated(boolean isAuthenticated); }
其實這三者很好區分,AuthenticationManager
主要就是爲了完成身份認證流程,ProviderManager
是 AuthenticationManager
接口的具體實現類,ProviderManager
裏面有個記錄 AuthenticationProvider
對象的集合屬性 providers
,AuthenticationProvider
接口類裏有兩個方法spa
public interface AuthenticationProvider { //實現具體的身份認證邏輯,認證失敗拋出對應的異常 Authentication authenticate(Authentication authentication) throws AuthenticationException; //該認證類是否支持該 Authentication 的認證 boolean supports(Class<?> authentication); }
接下來就是遍歷 ProviderManager
裏面的 providers
集合,找到和合適的 AuthenticationProvider
完成身份認證。線程
在 UserDetailsService
接口中只有一個簡單的方法
public interface UserDetailsService { //根據用戶名查到對應的 UserDetails 對象 UserDetails loadUserByUsername(String username) throws UsernameNotFoundException; }
對於上面概念有什麼不明白的地方,在們在接下來的流程中慢慢分析
在運行到 UsernamePasswordAuthenticationFilter
過濾器的時候首先是進入其父類 AbstractAuthenticationProcessingFilter
的 doFilter()
方法中
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { ... //首先配對是否是配置的身份認證的URI,是則執行下面的認證,不是則跳過 if (!requiresAuthentication(request, response)) { chain.doFilter(request, response); return; } ... Authentication authResult; try { //關鍵方法, 實現認證邏輯並返回 Authentication, 由其子類 UsernamePasswordAuthenticationFilter 實現, 由下面 5.3 詳解 authResult = attemptAuthentication(request, response); if (authResult == null) { // return immediately as subclass has indicated that it hasn't completed // authentication return; } sessionStrategy.onAuthentication(authResult, request, response); } catch (InternalAuthenticationServiceException failed) { //認證失敗調用...由下面 5.1 詳解 unsuccessfulAuthentication(request, response, failed); return; } catch (AuthenticationException failed) { //認證失敗調用...由下面 5.1 詳解 unsuccessfulAuthentication(request, response, failed); return; } // Authentication success if (continueChainBeforeSuccessfulAuthentication) { chain.doFilter(request, response); } //認證成功調用...由下面 5.2 詳解 successfulAuthentication(request, response, chain, authResult); }
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException { SecurityContextHolder.clearContext(); ... rememberMeServices.loginFail(request, response); //該 handler 處理失敗界面跳轉和響應邏輯 failureHandler.onAuthenticationFailure(request, response, failed); }
這裏默認配置的失敗處理 handler 是 SimpleUrlAuthenticationFailureHandler
,可自定義。
public class SimpleUrlAuthenticationFailureHandler implements AuthenticationFailureHandler { ... public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException { //沒有配置失敗跳轉的URL則直接響應錯誤 if (defaultFailureUrl == null) { logger.debug("No failure URL set, sending 401 Unauthorized error"); response.sendError(HttpStatus.UNAUTHORIZED.value(), HttpStatus.UNAUTHORIZED.getReasonPhrase()); } else { //不然 //緩存異常 saveException(request, exception); //根據配置的異常頁面是重定向仍是轉發進行不一樣方式跳轉 if (forwardToDestination) { logger.debug("Forwarding to " + defaultFailureUrl); request.getRequestDispatcher(defaultFailureUrl) .forward(request, response); } else { logger.debug("Redirecting to " + defaultFailureUrl); redirectStrategy.sendRedirect(request, response, defaultFailureUrl); } } } //緩存異常,轉發則保存在request裏面,重定向則保存在session裏面 protected final void saveException(HttpServletRequest request, AuthenticationException exception) { if (forwardToDestination) { request.setAttribute(WebAttributes.AUTHENTICATION_EXCEPTION, exception); } else { HttpSession session = request.getSession(false); if (session != null || allowSessionCreation) { request.getSession().setAttribute(WebAttributes.AUTHENTICATION_EXCEPTION, exception); } } } }
這裏作下小拓展:用系統的錯誤處理handler,指定認證失敗跳轉的URL,在MVC裏面對應的URL方法裏面能夠經過key從request
或session
裏面拿到錯誤信息,反饋給前端
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException { ... //這裏要注意很重要,將認證完成返回的 Authentication 保存到線程對應的 `SecurityContext` 中 SecurityContextHolder.getContext().setAuthentication(authResult); rememberMeServices.loginSuccess(request, response, authResult); // Fire event if (this.eventPublisher != null) { eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent( authResult, this.getClass())); } //該 handler 就是爲了完成頁面跳轉 successHandler.onAuthenticationSuccess(request, response, authResult); }
這裏默認配置的成功處理 handler 是 SavedRequestAwareAuthenticationSuccessHandler
,裏面的代碼就不作具體展開了,反正是跳轉到指定的認證成功以後的界面,可自定義。
public class UsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter { ... public static final String SPRING_SECURITY_FORM_USERNAME_KEY = "username"; public static final String SPRING_SECURITY_FORM_PASSWORD_KEY = "password"; private String usernameParameter = SPRING_SECURITY_FORM_USERNAME_KEY; private String passwordParameter = SPRING_SECURITY_FORM_PASSWORD_KEY; 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 封裝一個簡易的 AuthenticationToken UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken( username, password); // Allow subclasses to set the "details" property setDetails(request, authRequest); //具體的認證邏輯仍是交給 AuthenticationManager 對象的 authenticate(..) 方法完成,接着往下看 return this.getAuthenticationManager().authenticate(authRequest); } }
由源碼斷點跟蹤得知,最終解析是由 AuthenticationManager
接口實現類 ProviderManager
來完成
public class ProviderManager implements AuthenticationManager, MessageSourceAware, InitializingBean { ... private List<AuthenticationProvider> providers = Collections.emptyList(); ... public Authentication authenticate(Authentication authentication) throws AuthenticationException { .... //遍歷全部的 AuthenticationProvider, 找到合適的完成身份驗證 for (AuthenticationProvider provider : getProviders()) { if (!provider.supports(toTest)) { continue; } ... try { //進行具體的身份驗證邏輯, 這裏使用到的是 DaoAuthenticationProvider, 具體邏輯記着往下看 result = provider.authenticate(authentication); if (result != null) { copyDetails(authentication, result); break; } } catch ... } ... throw lastException; } }
DaoAuthenticationProvider
繼承自 AbstractUserDetailsAuthenticationProvider
實現了 AuthenticationProvider
接口
public abstract class AbstractUserDetailsAuthenticationProvider implements AuthenticationProvider, InitializingBean, MessageSourceAware { ... private UserDetailsChecker preAuthenticationChecks = new DefaultPreAuthenticationChecks(); private UserDetailsChecker postAuthenticationChecks = new DefaultPostAuthenticationChecks(); ... public Authentication authenticate(Authentication authentication) throws AuthenticationException { ... // 得到提交過來的用戶名 String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED" : authentication.getName(); //根據用戶名從緩存中查找 UserDetails boolean cacheWasUsed = true; UserDetails user = this.userCache.getUserFromCache(username); if (user == null) { cacheWasUsed = false; try { //緩存中沒有則經過 retrieveUser(..) 方法查找 (看下面 DaoAuthenticationProvider 的實現) user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication); } catch ... } try { //比對前的檢查,例如帳戶以一些狀態信息(是否鎖定, 過時...) preAuthenticationChecks.check(user); //子類實現比對規則 (看下面 DaoAuthenticationProvider 的實現) additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication); } catch (AuthenticationException exception) { if (cacheWasUsed) { // There was a problem, so try again after checking // we're using latest data (i.e. not from the cache) cacheWasUsed = false; user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication); preAuthenticationChecks.check(user); additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication); } else { throw exception; } } postAuthenticationChecks.check(user); if (!cacheWasUsed) { this.userCache.putUserInCache(user); } Object principalToReturn = user; if (forcePrincipalAsString) { principalToReturn = user.getUsername(); } //根據最終user的一些信息從新生成具體詳細的 Authentication 對象並返回 return createSuccessAuthentication(principalToReturn, authentication, user); } //具體生成仍是看子類實現 protected Authentication createSuccessAuthentication(Object principal, Authentication authentication, UserDetails user) { // Ensure we return the original credentials the user supplied, // so subsequent attempts are successful even with encoded passwords. // Also ensure we return the original getDetails(), so that future // authentication events after cache expiry contain the details UsernamePasswordAuthenticationToken result = new UsernamePasswordAuthenticationToken( principal, authentication.getCredentials(), authoritiesMapper.mapAuthorities(user.getAuthorities())); result.setDetails(authentication.getDetails()); return result; } }
接下來咱們來看下 DaoAuthenticationProvider
裏面的三個重要的方法,比對方式、獲取須要比對的 UserDetails
對象以及生產最終返回 Authentication
的方法。
public class DaoAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider { ... //密碼比對 @SuppressWarnings("deprecation") protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException { if (authentication.getCredentials() == null) { logger.debug("Authentication failed: no credentials provided"); throw new BadCredentialsException(messages.getMessage( "AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials")); } String presentedPassword = authentication.getCredentials().toString(); //經過 PasswordEncoder 進行密碼比對, 注: 可自定義 if (!passwordEncoder.matches(presentedPassword, userDetails.getPassword())) { logger.debug("Authentication failed: password does not match stored value"); throw new BadCredentialsException(messages.getMessage( "AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials")); } } //經過 UserDetailsService 獲取 UserDetails protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException { prepareTimingAttackProtection(); try { //經過 UserDetailsService 獲取 UserDetails UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username); if (loadedUser == null) { throw new InternalAuthenticationServiceException( "UserDetailsService returned null, which is an interface contract violation"); } return loadedUser; } catch (UsernameNotFoundException ex) { mitigateAgainstTimingAttack(authentication); throw ex; } catch (InternalAuthenticationServiceException ex) { throw ex; } catch (Exception ex) { throw new InternalAuthenticationServiceException(ex.getMessage(), ex); } } //生成身份認證經過後最終返回的 Authentication, 記錄認證的身份信息 @Override protected Authentication createSuccessAuthentication(Object principal, Authentication authentication, UserDetails user) { boolean upgradeEncoding = this.userDetailsPasswordService != null && this.passwordEncoder.upgradeEncoding(user.getPassword()); if (upgradeEncoding) { String presentedPassword = authentication.getCredentials().toString(); String newPassword = this.passwordEncoder.encode(presentedPassword); user = this.userDetailsPasswordService.updatePassword(user, newPassword); } return super.createSuccessAuthentication(principal, authentication, user); } }