您好,登錄后才能下訂單哦!
這篇文章主要介紹“Spring Security認證過程是什么”,在日常操作中,相信很多人在Spring Security認證過程是什么問題上存在疑惑,小編查閱了各式資料,整理出簡單好用的操作方法,希望對大家解答”Spring Security認證過程是什么”的疑惑有所幫助!接下來,請跟著小編一起來學習吧!
??還記得上一篇講解授權過程中提到@EnableWebSecurity 引用了 WebSecurityConfiguration 配置類 和 @EnableGlobalAuthentication 注解嗎? 當時只是講解了下 WebSecurityConfiguration 配置類 ,這次該輪到 @EnableGlobalAuthentication 配置了。
??查看 @EnableGlobalAuthentication 注解源碼,我們可以看到其引用了AuthenticationConfiguration 配置類。其中有一個方法值得我們注意,那就是 getAuthenticationManager() (還記得授權過程中調用了 AuthenticationManager().authenticate() 進行認證么?), 我們來看下其源碼內部大致邏輯:
public AuthenticationManager getAuthenticationManager() throws Exception { ...... // 1 調用 authenticationManagerBuilder 方法獲取 authenticationManagerBuilder 對象,用于 build authenticationManager 對象 AuthenticationManagerBuilder authBuilder = authenticationManagerBuilder( this.objectPostProcessor, this.applicationContext); ..... // 2 build 方法調用同授權過程中的 webSecurity.build() 一樣,都是通過父類 AbstractConfiguredSecurityBuilder.doBuild() 方法中的 performBuild() 方法進行 build, 只是這里不再是通過其子類 HttpSecurity.performBuild() ,而是通過 AuthenticationManagerBuilder.performBuild() authenticationManager = authBuilder.build(); ....... return authenticationManager; }
根據源碼我們可以概括其邏輯分2部分:
> - 1、 通過調用 authenticationManagerBuilder() 方法獲取 authenticationManagerBuilder 對象 > - 2、 調用authenticationManagerBuilder 對象的 build() 創建 authenticationManager 對象并返回
??我們再詳細看下這個build的過程,可以發現其 build 調用跟授權過程中build securityFilterChain 一樣 都是通過 AbstractConfiguredSecurityBuilder.doBuild() 方法中的 performBuild() 進行構建, 不過這次不再是調用其子類 HttpSecurity.performBuild() 而是 AuthenticationManagerBuilder.performBuild() 。 我們來看下 AuthenticationManagerBuilder.performBuild() 方法內部實現:
protected ProviderManager performBuild() throws Exception { if (!isConfigured()) { logger.debug("No authenticationProviders and no parentAuthenticationManager defined. Returning null."); return null; } // 1 創建了一個包含 authenticationProviders 參數 的 ProviderManager 對象 ProviderManager providerManager = new ProviderManager(authenticationProviders, parentAuthenticationManager); if (eraseCredentials != null) { providerManager.setEraseCredentialsAfterAuthentication(eraseCredentials); } if (eventPublisher != null) { providerManager.setAuthenticationEventPublisher(eventPublisher); } providerManager = postProcess(providerManager); return providerManager; }
?? 這里我們主要關注其內部 創建了一個包含 authenticationProviders 參數 的 ProviderManager (ProviderManager 是 AuthenticationManager 的實現類)對象并返回。
回過頭,我們來看下 AuthenticationManager 接口 源碼:
public interface AuthenticationManager { // 認證接口 Authentication authenticate(Authentication authentication) throws AuthenticationException; }
?? 可以看到,內部就只有一個我們在授權過程中提到過的 authenticate(),其接口接收一個 Authentication(這個對象我們也不陌生,之前授權過程中提到過的 UsernamePasswordAuthrnticationToken 等都是其實現子類) 對象作為參數。
??至此認證的部分關鍵類或接口已經浮出水面了,它們分別是 AuthenticationManager 、ProviderManager、AuthenticationProvider、Authentication, 接下來我們就圍繞這幾個類或接口進行剖析。
??正如我們之前看到的一項,它是整個認證的入口,其定義的認證接口 authenticate() 接收一個 Authentication 對象作為參數。AuthenticationManager 它只是提供了一個認證接口方法,因為在實際使用中,我們不僅有賬戶密碼的登錄方式,還有短信驗證碼登錄、郵箱登錄等等,所以它本身不做任何認證,其具體做認證的是 ProviderManager 子類,但正如我們說過的認證方式有很多,如果僅僅依靠 ProviderManager 本身來實現 authenticate() 接口,那我們要支持這么多認證方式不得寫多少個 if 判斷,而且以后如果我們想要支持指紋登錄,那又不得不在這個方法內部加個if,這種不利于系統擴展的寫法肯定是不可取的,所以 ProviderManager 本身會維護一個List<AuthenticationProvider>列表 ,用于存放多種認證方式,然后通過委托的方式,調用 AuthenticationProvider 來真正實現認證邏輯的 。 而 Authentication 就是我們需要認證的信息(當然不僅僅只包括賬戶信息),通過authenticate() 接口認證成功后返回的 Authentication 就是一個被標識認證成功的對象 。 這里為什么要解釋下 AuthenticationManager、ProviderManager、AuthenticationProvider 的關系,主要是一開始容易搞混它們,相信經過這樣一段描述更容易理解了吧。。。
?? 如果 沒有看過源碼的同學可能會認為 Authentication 是一個類吧,可實際上它是一個 接口,其內部并未存在任何屬性字段,它僅僅定義了和規范好了認證對象需要的接口方法,我們來看看其定義的接口方法有哪些,分別又什么作用:
public interface Authentication extends Principal, Serializable { // 1 獲取權限信息(不能僅僅理解未角色權限,還有菜單權限等等),默認是GrantedAuthority接口的實現類 Collection<!--? extends GrantedAuthority--> getAuthorities(); // 2 獲取用戶密碼信息 ,認證成功后會被刪除掉 Object getCredentials(); // 3 主要存放訪問著的ip等信息 Object getDetails(); // 4 重點!! 最重要的身份信息。 大部分情況下是 UserDetails 接口的實現 類,比如 我們 之前配置的 User 對象 Object getPrincipal(); // 5 是否認證(成功) boolean isAuthenticated(); // 6 設置認證標識 void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException; }
?? 既然 Authentication 定義了這些接口方法,那么其子類實現肯定都按照這個標準或者稱之為規范定制了實現,這里就不羅列出其子類的具體實現了,有興趣的同學可以去看下 我們最常用的 UsernamePasswordAuthenticationToken 實現(包括其 父類 AbstractAuthenticationToken)
?? 它是 AuthenticationManager 的實現子類之一,也是我們最常用的一個實現。正如我們前面提到過的,其內部維護了 一個 List<AuthenticationProvider> 對象, 用于支持和擴展 多種形式的認證方式。我們來看下 其 實現 authenticate() 的源碼:
public Authentication authenticate(Authentication authentication) throws AuthenticationException { ...... // 1 通過 getProviders() 方法獲取到內部維護的 List<authenticationprovider> 對象 并 通過遍歷的方式 去 認證,只要認證成功 就 break for (AuthenticationProvider provider : getProviders()) { // 2 正如前面看到的有 很多 AuthenticationProvider 實現,如果每次都是驗證失敗后再掉用下一個 AuthenticationProvider 這種實現是不是很不高效? 所以 這里通過 supports() 方法來驗證是否可以使用 該 AuthenticationProvider 進行驗證,不可以就直接換下一個 if (!provider.supports(toTest)) { continue; } try { // 3 重點,這里是 調用真實的認證方法 result = provider.authenticate(authentication); if (result != null) { copyDetails(authentication, result); break; } } catch (AccountStatusException e) { prepareException(e, authentication); throw e; } catch (InternalAuthenticationServiceException e) { prepareException(e, authentication); throw e; } catch (AuthenticationException e) { lastException = e; } } if (result == null && parent != null) { try { // 4 前面都認證不成功,調用父類(嚴格意思不是調用父類,而是其他的 AuthenticationManager 實現類)認證方法 result = parentResult = parent.authenticate(authentication); } catch (ProviderNotFoundException e) { } catch (AuthenticationException e) { lastException = parentException = e; } } if (result != null) { if (eraseCredentialsAfterAuthentication && (result instanceof CredentialsContainer)) { // 5 刪除認證成功后的 密碼信息,保證安全 ((CredentialsContainer) result).eraseCredentials(); } if (parentResult == null) { eventPublisher.publishAuthenticationSuccess(result); } return result; } if (lastException == null) { lastException = new ProviderNotFoundException(messages.getMessage( "ProviderManager.providerNotFound", new Object[] { toTest.getName() }, "No AuthenticationProvider found for {0}")); } if (parentException == null) { prepareException(lastException, authentication); } throw lastException; }
?? 梳理下整個方法內部實現邏輯:
> - 通過 getProviders() 方法獲取到內部維護的 List<authenticationprovider> 對象 并 通過遍歷的方式 去 認證 > - 通過 provider.supports() 方法 來驗證是否可用當前的 AuthenticationProvider 進行驗證,不可以就直接換下一個 ( 其實方法內部就是驗證當前 的 Authentication 對象是不是其某個子類,比如 我們最常用到的 DaoAuthenticationProvider 的 supports 方法就是判斷當前 的 Authentication 是不是 UsernamePasswordAuthenticationToken ) > - 通過 provider.authenticate() 調用 其真正的認證實現 > - 如果 前面的所有 AuthenticationProvider 均不能認證成功,嘗試調用 parent.authenticate() 方法 :調用父類(嚴格意思不是調用父類,而是其他的 AuthenticationManager 實現類)認證方法 > - 最后 通過 ((CredentialsContainer) result).eraseCredentials() 刪除認證成功后的 密碼信息,保證安全
?? 正如我們想象的一樣,AuthenticationProvider 是一個接口,本身定義了一個 和 AuthenticationManager 一樣的 authenticate 認證接口方法,外加一個 supports() 用于 判別當前 Authentication 是否可以進行處理。
public interface AuthenticationProvider { // 定義認證接口方法 Authentication authenticate(Authentication authentication) throws AuthenticationException; // 定義判斷是否可以認證處理的接口方法 boolean supports(Class<!--?--> authentication); }
?? 這里我們就拿我們用得最多的一個 AuthenticationProvider 實現類 DaoAuthenticationProvider(注意,這里和UsernamePasswordAuthenticationFilter 類似,都是通過父類來實現接口,然后內部處理方法再調用 其 子類進行處理) 來看其內部 這2個抽象方法的實現:
supports 實現:
public boolean supports(Class<!--?--> authentication) { return (UsernamePasswordAuthenticationToken.class .isAssignableFrom(authentication)); }
?? 可以看到僅僅只是判斷當前的 authentication 是否為 UsernamePasswordAuthenticationToken(或其子類)
authrnticate 實現
// 1 注意這里的實現方法是 DaoAuthenticationProvider 的父類 AbstractUserDetailsAuthenticationProvider 實現的 public Authentication authenticate(Authentication authentication) throws AuthenticationException { // 2 從 authentication 中獲取 用戶名 String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED" : authentication.getName(); boolean cacheWasUsed = true; // 3 根據username 從緩存中獲取 認證成功的 UserDetails 信息 UserDetails user = this.userCache.getUserFromCache(username); if (user == null) { cacheWasUsed = false; try { // 4 如果緩存中沒有用戶信息 需要 獲取用戶信息(由 DaoAuthenticationProvider 實現 ) user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication); } catch (UsernameNotFoundException notFound) { ...... } } try { // 5 前置檢查賬戶是否鎖定,過期,凍結(由DefaultPreAuthenticationChecks類實現) preAuthenticationChecks.check(user); // 6 主要是驗證 獲取到的用戶密碼與傳入的用戶密碼是否一致 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; } } // 7 后置檢查用戶密碼是否 過期 postAuthenticationChecks.check(user); // 8 驗證成功后的用戶信息存入緩存 if (!cacheWasUsed) { this.userCache.putUserInCache(user); } Object principalToReturn = user; if (forcePrincipalAsString) { principalToReturn = user.getUsername(); } // 9 重新創建一個 authenticated 為true (即認證成功)的 UsernamePasswordAuthenticationToken 對象并返回 return createSuccessAuthentication(principalToReturn, authentication, user); }
?? 梳理下authenticate(這里的方法的實現是由 AbstractUserDetailsAuthenticationProvider 提供的)方法內部實現邏輯:
> - 從 入參 authentication 對象中獲取到 username 信息 > - (這里忽略緩存的處理) 調用 retrieveUser() 方法(由 DaoAuthenticationProvider 實現)根據 username 獲取到 系統(一般來說是從數據庫中) 中獲取到 UserDetails 對象 > - 通過 preAuthenticationChecks.check() 方法檢測 當前獲取到的 UserDetails 是否過期、凍結、鎖定(如果任意一個條件 為 true 將拋出 相應 的異常) > - 通過 additionalAuthenticationChecks() (由 DaoAuthenticationProvider 實現) 判斷 密碼是否一致 > - 通過 postAuthenticationChecks.check() 檢測 UserDetails 的密碼是否過期 > - 最后通過 createSuccessAuthentication() 重新創建一個 authenticated 為true (即認證成功)的 UsernamePasswordAuthenticationToken 對象并返回
??雖然我們知道其驗證邏輯, 但其內部很多方法我們不清楚其內部實現,以及這里新增的一個 關鍵認證類 UserDetails 是怎么設計的,如何驗證其是否過期等等。
??繼續深入看下 retrieveUser() 方法,首先我們注意到其返回對象是一個 UserDetails,那么我們先從 UserDetails 入手。
?? 我們先來看下 UserDetails 源碼:
public interface UserDetails extends Serializable { // 1 與 Authentication 的 一樣,都是獲取 權限信息 Collection<!--? extends GrantedAuthority--> getAuthorities(); // 2 獲取用戶正確的密碼 String getPassword(); // 3 獲取賬戶名 String getUsername(); // 4 賬戶是否過期 boolean isAccountNonExpired(); // 5 賬戶是否鎖定 boolean isAccountNonLocked(); // 6 密碼是否過期 boolean isCredentialsNonExpired(); // 7 賬戶是否凍結 boolean isEnabled(); }
?? 從上面的 4,5,6,7 接口我們就能夠知道 preAuthenticationChecks.check() 和 postAuthenticationChecks.check() 是如何檢測的了,這里2個方法的檢測細節就不再深究了,有興趣的同學可以看看源碼,我們只要知道檢測失敗會拋出異常就行了。
??咋呼一看,這個UserDetails 和 Authentication 很相似,其實它們之間還真有關系,在createSuccessAuthentication() 傳教Authentication 對象時,它的authorities 就是UserDetails 傳入的。
??retrieveUser() 方法是系統通過傳入的賬戶名獲取對應的賬戶信息的唯一方法,我們來看下其內部源碼邏輯:
protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException { prepareTimingAttackProtection(); try { // 通過 UserDetailsService 的loadUserByUsername 方法 獲取用戶信息 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) { ...... } }
?? 相信看到這里,一切都關聯上了,這里的 UserDetailsService.loadUserByUsername() 就是我們在 上一篇 授權過程中 我們自己實現的。 這里就不再 貼出UserDetailsService 源碼了。
?? 還有additionalAuthenticationChecks() 密碼驗證沒有講到,這里簡單提下,其內部就是通過 PasswordEncoder.matches() 方法進行密碼匹配的。不過這里要注意一下,這里的 PasswordEncoder 在 Security 5 開始默認 替換成了 DelegatingPasswordEncoder 這里也是和我們之前 討論 loadUserByUsername 方法內部創建User (UserDeatails 實現類之一)是一定要用到 PasswordEncoderFactories.createDelegatingPasswordEncoder().encode() 加密是相應的。
到此,關于“Spring Security認證過程是什么”的學習就結束了,希望能夠解決大家的疑惑。理論與實踐的搭配能更好的幫助大家學習,快去試試吧!若想繼續學習更多相關知識,請繼續關注億速云網站,小編會繼續努力為大家帶來更多實用的文章!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。