一些許可權框架一般都包含認證器和決策器,前者處理登陸驗證,後者處理存取資源的控制
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
,在通過具體的實現類來處理請求,在這個過程中,將查詢使用者的實現和驗證程式碼分離開來
整個過程看著像是策略模式,後邊將變化的部分抽離出來,實現解耦
前邊提到的認證成功會呼叫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自定義認證器