在學習Spring Cloud 時,遇到了受權服務oauth 相關內容時,老是隻知其一;不知其二,所以決定先把Spring Security 、Spring Security Oauth2 等權限、認證相關的內容、原理及設計學習並整理一遍。本系列文章就是在學習的過程當中增強印象和理解所撰寫的,若有侵權請告知。git
項目環境:github
- JDK1.8
- Spring boot 2.x
- Spring Security 5.x
還記得上一篇講解 受權過程 中提到@EnableWebSecurity 引用了 WebSecurityConfiguration 配置類 和 @EnableGlobalAuthentication 註解嗎? 當時只是講解了下 WebSecurityConfiguration 配置類 ,此次該輪到 @EnableGlobalAuthentication 配置了。web
查看 @EnableGlobalAuthentication 註解源碼,咱們能夠看到其引用了AuthenticationConfiguration 配置類。其中有一個方法值得咱們注意,那就是 getAuthenticationManager() (還記得受權過程當中調用了 AuthenticationManager().authenticate() 進行認證麼?), 咱們來看下其源碼內部大體邏輯:spring
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部分:數據庫
- 一、 經過調用 authenticationManagerBuilder() 方法獲取 authenticationManagerBuilder 對象
- 二、 調用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 接口 源碼:bash
public interface AuthenticationManager {
// 認證接口
Authentication authenticate(Authentication authentication)
throws AuthenticationException;
}
複製代碼
能夠看到,內部就只有一個咱們在受權過程當中提到過的 authenticate(),其接口接收一個 Authentication(這個對象咱們也不陌生,以前受權過程當中提到過的 UsernamePasswordAuthrnticationToken 等都是其實現子類) 對象做爲參數。app
至此認證的部分關鍵類或接口已經浮出水面了,它們分別是 AuthenticationManager 、ProviderManager、AuthenticationProvider、Authentication, 接下來咱們就圍繞這幾個類或接口進行剖析。ide
正如咱們以前看到的一項,它是整個認證的入口,其定義的認證接口 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 對象 並 經過遍歷的方式 去 認證
- 經過 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個抽象方法的實現:
public boolean supports(Class<?> authentication) {
return (UsernamePasswordAuthenticationToken.class
.isAssignableFrom(authentication));
}
複製代碼
能夠看到僅僅只是判斷當前的 authentication 是否爲 UsernamePasswordAuthenticationToken(或其子類)
// 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() 加密是相應的。
認證的頂級管理員 AuthenticationManager 爲咱們提供了 認證入口( authenticate()接口),可是呢,咱們也知道大老闆通常不直接參與實質的工做,因此它把任務安排給它的下屬,也就是咱們的 ProviderManager 部門領導 ,部門領導 肩負起 認證的工做(authenticate() 認證的實現),其實呢,咱們也知道部門領導也是 直接參數 認證工做的,它都是將實際任務安排給小組長的, 也就是咱們的 AuthrnticationProvider ,部門領導 開個會議,彙集了全部小組長 ,讓它們自行判斷(經過 support()) 大老闆交下來的任務 該由誰來完成, 小組長 領到任務後,就把任務 分發給各個小組成員,好比 成員1(UserDetailsService) 只須要 完成 retrieveUser() 的工做,而後成員2 完成 additionalAuthenticationChecks() 的工做,最後由項目經理 ( createSuccessAuthentication() ) 將結果彙報給小組長,而後小組長彙報給部門領導,部門領導 審覈一下結果,以爲小組長作得不夠好,而後又作了一些操做 ( eraseCredentials() 擦除密碼信息 ),最後認爲 結果 能夠了就彙報給老闆,老闆呢,也很少看,直接將結果給了客戶(filter)。
按照慣例,上流程圖:
本文介紹認證過程的代碼能夠訪問代碼倉庫中的 security 模塊 ,項目的github 地址 : github.com/BUG9/spring…
若是您對這些感興趣,歡迎star、follow、收藏、轉發給予支持!