Spring Security認證器實現
一些權限框架一般都包含認證器和決策器,前者處理登陸驗證,後者處理訪問資源的控制
Spring Security的登陸請求處理如圖
下面來分析一下是怎麼實現認證器的
攔截請求
首先登陸請求會被UsernamePasswordAuthenticationFilter
攔截,這個過濾器看名字就知道是一個攔截用戶名密碼的攔截器
主要的驗證是在attemptAuthentication()
方法里,他會去獲取在請求中的用戶名密碼,並且創建一個該用戶的上下文,然後在去執行一個驗證過程
String username = this.obtainUsername(request);
String password = this.obtainPassword(request);
//創建上下文
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
this.setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
可以看看UsernamePasswordAuthenticationToken
這個類,他是繼承了AbstractAuthenticationToken
,然後這個父類實現了Authentication
由這個類的方法和屬性可得知他就是存儲用戶驗證信息的,認證器的主要功能應該就是驗證完成後填充這個類
回到UsernamePasswordAuthenticationToken
中,在上面創建的過程了可以發現
public UsernamePasswordAuthenticationToken(Object principal,Object credentials){
super(null);
this.principal=principal;
this.credentials=credentials;
//還沒認證
setAuthenticated(false);
}
還有一個super(null)
的處理,因為剛進來是還不知道有什麼權限的,設置null是初始化一個空的權限
//權限利集合
private final Collection<GrantedAuthority> authorities;
//空的集合
public static final List<GrantedAuthority> NO_AUTHORITIES = Collections.emptyList();
//初始化
if (authorities == null) {
this.authorities = AuthorityUtils.NO_AUTHORITIES;
return;
}
那麼後續認證完還會把權限設置盡量,此時可以看UsernamePasswordAuthenticationToken
的另一個重載構造器
//認證完成
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
}
在看源碼的過程中,注釋一直在強調這些上下文的填充和設置都應該是由AuthenticationManager
或者AuthenticationProvider
的實現類去操作
驗證過程
接下來會把球踢給AuthenticationManager
,但他只是個接口
/**
* Attempts to authenticate the passed {@link Authentication} object, returning a
* fully populated <code>Authentication</code> object (including granted authorities)
* if successful.
**/
public interface AuthenticationManager {
Authentication authenticate(Authentication authentication)
throws AuthenticationException;
}
注釋也寫的很清楚了,認證完成後會填充Authentication
接下來會委託給ProviderManager
,因為他實現了AuthenticationManager
剛進來看authenticate()
方法會發現他先遍歷了一個List<AuthenticationProvider>
集合
/**
* Indicates a class can process a specific Authentication
**/
public interface AuthenticationProvider {
Authentication authenticate(Authentication authentication)
throws AuthenticationException;
//支不支持特定類型的authentication
boolean supports(Class<?> authentication);
}
實現這個類就可以處理不同類型的Authentication
,比如上邊的UsernamePasswordAuthenticationToken
,對應的處理類是AbstractUserDetailsAuthenticationProvider
,為啥知道呢,因為在這個supports()
里
public boolean supports(Class<?> authentication) {
return (UsernamePasswordAuthenticationToken.class
.isAssignableFrom(authentication));
}
注意到這個是抽象類,實際的處理方法是在他的子類DaoAuthenticationProvider
里,但是最重要的authenticate()
方法子類好像沒有繼承,看看父類是怎麼實現這個方法的
-
首先是繼續判斷
Authentication
是不是特定的類Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication, () -> messages.getMessage( "AbstractUserDetailsAuthenticationProvider.onlySupports", "Only UsernamePasswordAuthenticationToken is supported"));
-
查詢根據用戶名用戶,這次就是到了子類的方法了,因為這個方法是抽象的
user=retrieveUser(username, (UsernamePasswordAuthenticationToken)authentication);
接着
DaoAuthenticationProvider
會調用真正實現查詢用戶的類UserDetailsService
UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
UserDetailsService
這個類信息就不陌生了,我們一般都會去實現這個類來自定義查詢用戶的方式,查詢完後會返回一個UserDetails
,當然也可以繼承這個類來擴展想要的字段,主要填充的是權限信息和密碼 -
檢驗用戶,如果獲取到的
UserDetails
是null,則拋異常,不為空則繼續校驗//檢驗用戶合法性 preAuthenticationChecks.check(user); //校驗密碼 additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication);
第一個教育是判斷用戶的合法性,就是判斷
UserDetails
里的幾個字段//賬號是否過期 boolean isAccountNonExpired(); //賬號被鎖定或解鎖狀態。 boolean isAccountNonLocked(); //密碼是否過期 boolean isCredentialsNonExpired(); //是否啟用 boolean isEnabled();
第二個則是由子類實現的,判斷從數據庫獲取的密碼和請求中的密碼是否一致,因為用的登陸方式是根據用戶名稱登陸,所以有檢驗密碼的步驟
String presentedPassword = authentication.getCredentials().toString(); 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")); }
需要主要的是請求中的密碼是被加密過的,所以從數據庫獲取到的密碼也應該是被加密的
注意到當完成校驗的時候會把信息放入緩存
//當沒有從緩存中獲取到值時,這個字段會被設置成false if (!cacheWasUsed) { this.userCache.putUserInCache(user); } //下次進來的時候回去獲取 UserDetails user = this.userCache.getUserFromCache(username);
如果是從緩存中獲取,也是會走檢驗邏輯的
最後完成檢驗,並填充一個完整的
Authentication
return createSuccessAuthentication(principalToReturn, authentication, user);
由上述流程來看,Security的檢驗過程還是比較清晰的,通過AuthenticationManager
來委託給ProviderManager
,在通過具體的實現類來處理請求,在這個過程中,將查詢用戶的實現和驗證代碼分離開來
整個過程看着像是策略模式,後邊將變化的部分抽離出來,實現解耦
返回完整的Authentication
前邊提到的認證成功會調用createSuccessAuthentication()
方法,裡邊的內容很簡單
UsernamePasswordAuthenticationToken result = new UsernamePasswordAuthenticationToken(
principal, authentication.getCredentials(),
authoritiesMapper.mapAuthorities(user.getAuthorities()));
result.setDetails(authentication.getDetails());
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
}
這次往supe里放了權限集合,父類的處理是判斷裡邊的權限有沒有空的,沒有則轉換為只讀集合
for (GrantedAuthority a : authorities) {
if (a == null) {
throw new IllegalArgumentException(
"Authorities collection cannot contain any null elements");
}
}
ArrayList<GrantedAuthority> temp = new ArrayList<>(
authorities.size());
temp.addAll(authorities);
this.authorities = Collections.unmodifiableList(temp);
收尾工作
回到ProviderManager里的authenticate方法,當我們終於從
result = provider.authenticate(authentication);
走出來時,後邊還有什麼操作
- 將返回的用戶信息負責給當前的上下文
if (result != null) {
copyDetails(authentication, result);
break;
}
-
刪除敏感信息
((CredentialsContainer) result).eraseCredentials();
這個過程會將一些字段設置為null,可以實現
eraseCredentials()
方法來自定義需要刪除的信息
最後返回到UsernamePasswordAuthenticationFilter
中通過過濾
結論
這就是Spring Security實現認證的過程了
通過實現自己的上下文Authentication
和處理類AuthenticationProvider
以及具體的查詢用戶的方法就可以自定義自己的登陸實現
具體可以看Spring Security自定義認證器