摘要:通過上面我們知道對于表單登錄的認證請求是交給了處理的,那么具體的認證流程如下從上圖可知,繼承于抽象類。中維護這一個對象列表,通過遍歷判斷并且最后選擇對象來完成最后的認證。發布一個登錄事件。
概要
前面一節,通過簡單配置即可實現SpringSecurity表單認證功能,而今天這一節將通過閱讀源碼的形式來學習SpringSecurity是如何實現這些功能, 前方高能預警,本篇分析源碼篇幅較長。
過濾器鏈前面我說過SpringSecurity是基于過濾器鏈的形式,那么我解析將會介紹一下具體有哪些過濾器。
Filter Class | 介紹 |
---|---|
SecurityContextPersistenceFilter | 判斷當前用戶是否登錄 |
CrsfFilter | 用于防止csrf攻擊 |
LogoutFilter | 處理注銷請求 |
UsernamePasswordAuthenticationFilter | 處理表單登錄的請求(也是我們今天的主角) |
BasicAuthenticationFilter | 處理http basic認證的請求 |
由于過濾器鏈中的過濾器實在太多,我沒有一一列舉,調了幾個比較重要的介紹一下。
通過上面我們知道SpringSecurity對于表單登錄的認證請求是交給了UsernamePasswordAuthenticationFilter處理的,那么具體的認證流程如下:
從上圖可知,UsernamePasswordAuthenticationFilter繼承于抽象類AbstractAuthenticationProcessingFilter。
具體認證是:
進入doFilter方法,判斷是否要認證,如果需要認證則進入attemptAuthentication方法,如果不需要直接結束
attemptAuthentication方法中根據username跟password構造一個UsernamePasswordAuthenticationToken對象(此時的token是未認證的),并且將它交給ProviderManger來完成認證。
ProviderManger中維護這一個AuthenticationProvider對象列表,通過遍歷判斷并且最后選擇DaoAuthenticationProvider對象來完成最后的認證。
DaoAuthenticationProvider根據ProviderManger傳來的token取出username,并且調用我們寫的UserDetailsService的loadUserByUsername方法從數據庫中讀取用戶信息,然后對比用戶密碼,如果認證通過,則返回用戶信息也是就是UserDetails對象,在重新構造UsernamePasswordAuthenticationToken(此時的token是 已經認證通過了的)。
接下來我們將通過源碼來分析具體的整個認證流程。
AbstractAuthenticationProcessingFilterAbstractAuthenticationProcessingFilter 是一個抽象類。所有的認證認證請求的過濾器都會繼承于它,它主要將一些公共的功能實現,而具體的驗證邏輯交給子類實現,有點類似于父類設置好認證流程,子類負責具體的認證邏輯,這樣跟設計模式的模板方法模式有點相似。
現在我們分析一下 它里面比較重要的方法
1、doFilterpublic void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { // 省略不相干代碼。。。 // 1、判斷當前請求是否要認證 if (!requiresAuthentication(request, response)) { // 不需要直接走下一個過濾器 chain.doFilter(request, response); return; } try { // 2、開始請求認證,attemptAuthentication具體實現給子類,如果認證成功返回一個認證通過的Authenticaion對象 authResult = attemptAuthentication(request, response); if (authResult == null) { return; } // 3、登錄成功 將認證成功的用戶信息放入session SessionAuthenticationStrategy接口,用于擴展 sessionStrategy.onAuthentication(authResult, request, response); } catch (InternalAuthenticationServiceException failed) { //2.1、發生異常,登錄失敗,進入登錄失敗handler回調 unsuccessfulAuthentication(request, response, failed); return; } catch (AuthenticationException failed) { //2.1、發生異常,登錄失敗,進入登錄失敗處理器 unsuccessfulAuthentication(request, response, failed); return; } // 3.1、登錄成功,進入登錄成功處理器。 successfulAuthentication(request, response, chain, authResult); }2、successfulAuthentication
登錄成功處理器
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException { //1、登錄成功 將認證成功的Authentication對象存入SecurityContextHolder中 // SecurityContextHolder本質是一個ThreadLocal SecurityContextHolder.getContext().setAuthentication(authResult); //2、如果開啟了記住我功能,將調用rememberMeServices的loginSuccess 將生成一個token // 將token放入cookie中這樣 下次就不用登錄就可以認證。具體關于記住我rememberMeServices的相關分析我 們下面幾篇文章會深入分析的。 rememberMeServices.loginSuccess(request, response, authResult); // Fire event //3、發布一個登錄事件。 if (this.eventPublisher != null) { eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent( authResult, this.getClass())); } //4、調用我們自己定義的登錄成功處理器,這樣也是我們擴展得知登錄成功的一個擴展點。 successHandler.onAuthenticationSuccess(request, response, authResult); }3、unsuccessfulAuthentication
登錄失敗處理器
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException { //1、登錄失敗,將SecurityContextHolder中的信息清空 SecurityContextHolder.clearContext(); //2、關于記住我功能的登錄失敗處理 rememberMeServices.loginFail(request, response); //3、調用我們自己定義的登錄失敗處理器,這里可以擴展記錄登錄失敗的日志。 failureHandler.onAuthenticationFailure(request, response, failed); }
關于AbstractAuthenticationProcessingFilter主要分析就到這。我們可以從源碼中知道,當請求進入該過濾器中具體的流程是
判斷該請求是否要被認證
調用attemptAuthentication方法開始認證,由于是抽象方法具體認證邏輯給子類
如果登錄成功,則將認證結果Authentication對象根據session策略寫入session中,將認證結果寫入到SecurityContextHolder,如果開啟了記住我功能,則根據記住我功能,生成token并且寫入cookie中,最后調用一個successHandler對象的方法,這個對象可以是我們配置注入的,用于處理我們的自定義登錄成功的一些邏輯(比如記錄登錄成功日志等等)。
如果登錄失敗,則清空SecurityContextHolder中的信息,并且調用我們自己注入的failureHandler對象,處理我們自己的登錄失敗邏輯。
UsernamePasswordAuthenticationFilter從上面分析我們可以知道,UsernamePasswordAuthenticationFilter是繼承于AbstractAuthenticationProcessingFilter,并且實現它的attemptAuthentication方法,來實現認證具體的邏輯實現。接下來,我們通過閱讀UsernamePasswordAuthenticationFilter的源碼來解讀,它是如何完成認證的。 由于這里會涉及UsernamePasswordAuthenticationToken對象構造,所以我們先看看UsernamePasswordAuthenticationToken的源碼
1、UsernamePasswordAuthenticationToken// 繼承至AbstractAuthenticationToken // AbstractAuthenticationToken主要定義一下在SpringSecurity中toke需要存在一些必須信息 // 例如權限集合 Collectionauthorities; 是否認證通過boolean authenticated = false;認證通過的用戶信息Object details; public class UsernamePasswordAuthenticationToken extends AbstractAuthenticationToken { // 未登錄情況下 存的是用戶名 登錄成功情況下存的是UserDetails對象 private final Object principal; // 密碼 private Object credentials; /** * 構造函數,用戶沒有登錄的情況下,此時的authenticated是false,代表尚未認證 */ public UsernamePasswordAuthenticationToken(Object principal, Object credentials) { super(null); this.principal = principal; this.credentials = credentials; setAuthenticated(false); } /** * 構造函數,用戶登錄成功的情況下,多了一個參數 是用戶的權限集合,此時的authenticated是true,代表認證成功 */ public UsernamePasswordAuthenticationToken(Object principal, Object credentials, Collection extends GrantedAuthority> authorities) { super(authorities); this.principal = principal; this.credentials = credentials; super.setAuthenticated(true); // must use super, as we override } }
接下來我們就可以分析attemptAuthentication方法了。
2、attemptAuthenticationpublic Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { // 1、判斷是不是post請求,如果不是則拋出AuthenticationServiceException異常,注意這里拋出的異常都在AbstractAuthenticationProcessingFilter#doFilter方法中捕獲,捕獲之后會進入登錄失敗的邏輯。 if (postOnly && !request.getMethod().equals("POST")) { throw new AuthenticationServiceException( "Authentication method not supported: " + request.getMethod()); } // 2、從request中拿用戶名跟密碼 String username = obtainUsername(request); String password = obtainPassword(request); // 3、非空處理,防止NPE異常 if (username == null) { username = ""; } if (password == null) { password = ""; } // 4、除去空格 username = username.trim(); // 5、根據username跟password構造出一個UsernamePasswordAuthenticationToken對象 從上文分析可知道,此時的token是未認證的。 UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken( username, password); // 6、配置一下其他信息 ip 等等 setDetails(request, authRequest); // 7、調用ProviderManger的authenticate的方法進行具體認證邏輯 return this.getAuthenticationManager().authenticate(authRequest); }ProviderManager
維護一個AuthenticationProvider列表,進行認證邏輯驗證
1、authenticatepublic Authentication authenticate(Authentication authentication) throws AuthenticationException { // 1、拿到token的類型。 Class extends Authentication> toTest = authentication.getClass(); AuthenticationException lastException = null; Authentication result = null; // 2、遍歷AuthenticationProvider列表 for (AuthenticationProvider provider : getProviders()) { // 3、AuthenticationProvider不支持當前token類型,則直接跳過 if (!provider.supports(toTest)) { continue; } try { // 4、如果Provider支持當前token,則交給Provider完成認證。 result = provider.authenticate(authentication); } catch (AccountStatusException e) { throw e; } catch (InternalAuthenticationServiceException e) { throw e; } catch (AuthenticationException e) { lastException = e; } } // 5、登錄成功 返回登錄成功的token if (result != null) { eventPublisher.publishAuthenticationSuccess(result); return result; } }AbstractUserDetailsAuthenticationProvider 1、authenticate
AbstractUserDetailsAuthenticationProvider實現了AuthenticationProvider接口,并且實現了部分方法,DaoAuthenticationProvider繼承于AbstractUserDetailsAuthenticationProvider類,所以我們先來看看AbstractUserDetailsAuthenticationProvider的實現。
public abstract class AbstractUserDetailsAuthenticationProvider implements AuthenticationProvider, InitializingBean, MessageSourceAware { // 國際化處理 protected MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor(); /** * 對token一些檢查,具體檢查邏輯交給子類實現,抽象方法 */ protected abstract void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException; /** * 認證邏輯的實現,調用抽象方法retrieveUser根據username獲取UserDetails對象 */ public Authentication authenticate(Authentication authentication) throws AuthenticationException { // 1、獲取usernmae String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED" : authentication.getName(); // 2、嘗試去緩存中獲取UserDetails對象 UserDetails user = this.userCache.getUserFromCache(username); // 3、如果為空,則代表當前對象沒有緩存。 if (user == null) { cacheWasUsed = false; try { //4、調用retrieveUser去獲取UserDetail對象,為什么這個方法是抽象方法大家很容易知道,如果UserDetail信息存在關系數據庫 則可以重寫該方法并且去關系數據庫獲取用戶信息,如果UserDetail信息存在其他地方,可以重寫該方法用其他的方法去獲取用戶信息,這樣絲毫不影響整個認證流程,方便擴展。 user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication); } catch (UsernameNotFoundException notFound) { // 捕獲異常 日志處理 并且往上拋出,登錄失敗。 if (hideUserNotFoundExceptions) { throw new BadCredentialsException(messages.getMessage( "AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials")); } else { throw notFound; } } } try { // 5、前置檢查 判斷當前用戶是否鎖定,禁用等等 preAuthenticationChecks.check(user); // 6、其他的檢查,在DaoAuthenticationProvider是檢查密碼是否一致 additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication); } catch (AuthenticationException exception) { } // 7、后置檢查,判斷密碼是否過期 postAuthenticationChecks.check(user); // 8、登錄成功通過UserDetail對象重新構造一個認證通過的Token對象 return createSuccessAuthentication(principalToReturn, authentication, user); } protected Authentication createSuccessAuthentication(Object principal, Authentication authentication, UserDetails user) { // 調用第二個構造方法,構造一個認證通過的Token對象 UsernamePasswordAuthenticationToken result = new UsernamePasswordAuthenticationToken( principal, authentication.getCredentials(), authoritiesMapper.mapAuthorities(user.getAuthorities())); result.setDetails(authentication.getDetails()); return result; } }
接下來我們具體看看retrieveUser的實現,沒看源碼大家應該也可以知道,retrieveUser方法應該是調用UserDetailsService去數據庫查詢是否有該用戶,以及用戶的密碼是否一致。
DaoAuthenticationProviderDaoAuthenticationProvider 主要是通過UserDetailService來獲取UserDetail對象。
1、retrieveUserprotected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException { try { // 1、調用UserDetailsService接口的loadUserByUsername方法獲取UserDeail對象 UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username); // 2、如果loadedUser為null 代表當前用戶不存在,拋出異常 登錄失敗。 if (loadedUser == null) { throw new InternalAuthenticationServiceException( "UserDetailsService returned null, which is an interface contract violation"); } // 3、返回查詢的結果 return loadedUser; } }2、additionalAuthenticationChecks
protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException { // 1、如果密碼為空,則拋出異常、 if (authentication.getCredentials() == null) { throw new BadCredentialsException(messages.getMessage( "AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials")); } // 2、獲取用戶輸入的密碼 String presentedPassword = authentication.getCredentials().toString(); // 3、調用passwordEncoder的matche方法 判斷密碼是否一致 if (!passwordEncoder.matches(presentedPassword, userDetails.getPassword())) { logger.debug("Authentication failed: password does not match stored value"); // 4、如果不一致 則拋出異常。 throw new BadCredentialsException(messages.getMessage( "AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials")); } }總結
至此,整認證流程已經分析完畢,大家如果有什么不懂可以關注我的公眾號一起討論。
學習是一個漫長的過程,學習源碼可能會很困難但是只要努力一定就會有獲取,大家一致共勉。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/74129.html
摘要:里面配置的過濾器鏈當用戶使用表單請求時進入返回一個的實例一般是從數據庫中查詢出來的實例然后直接到最后一個如果有錯則拋錯給前面一個進行拋錯如果沒有錯則放行可以訪問對應的資源上面是總的執行流程下面單獨說一下的認證流程這個圖應該都看得懂和里面的配 showImg(https://segmentfault.com/img/bVbvO0O?w=1258&h=261);web.xml里面配置的過濾...
摘要:什么是是一個能夠為基于的企業應用系統提供聲明式的安全訪問控制解決方案的安全框架。它來自于,那么它與整合開發有著天然的優勢,目前與對應的開源框架還有。通常大家在做一個后臺管理的系統的時候,應該采用判斷用戶是否登錄。 ? 什么是SpringSecurity ? ? Spring Security是一個能夠為基于Spring的企業應用系統提供聲明式的安全訪問控制解決方案的安全...
摘要:前言現在的好多項目都是基于移動端以及前后端分離的項目,之前基于的前后端放到一起的項目已經慢慢失寵并淡出我們視線,尤其是當基于的微服務架構以及單頁面應用流行起來后,情況更甚。使用生成是什么請自行百度。 1、前言 現在的好多項目都是基于APP移動端以及前后端分離的項目,之前基于Session的前后端放到一起的項目已經慢慢失寵并淡出我們視線,尤其是當基于SpringCloud的微服務架構以及...
摘要:建立一個模塊繼承上一個模塊然后添加依賴解決打包時找不到文件建立數據源文件數據庫連接相關修改配置數據源和整合,以及事務管理自動掃描掃描時跳過注解的類控制器掃描配置文件這里指向的是 1.建立一個模塊繼承上一個模塊然后添加依賴 junit junit 4.11 test ...
摘要:創建一個工程在里面添加依賴,依賴不要隨便改我改了出錯了好幾次都找不到原因可以輕松的將對象轉換成對象和文檔同樣也可以將轉換成對象和配置 1.創建一個web工程2.在pom里面添加依賴,依賴不要隨便改,我改了出錯了好幾次都找不到原因 UTF-8 1.7 1.7 2.5.0 1.2 3.0-alpha-1 ...
閱讀 2432·2021-11-18 10:02
閱讀 690·2021-10-08 10:04
閱讀 2257·2021-09-03 10:51
閱讀 3547·2019-08-30 15:44
閱讀 2804·2019-08-29 14:09
閱讀 2469·2019-08-29 12:21
閱讀 2067·2019-08-26 13:45
閱讀 1805·2019-08-26 13:25