AuthenticationManager相關類圖
- AuthenticationManager驗證過程
AuthenticationManager驗證過程涉及到的類和接口較多,我們就從這里開始逐一分析,首先我手畫了一張圖作為索引,這張圖說明了各個類和接口之間的關系。
- AuthenticationManager 為認證管理接口類,其定義了認證方法authenticate()。
- ProviderManager 為驗證管理類,實現了接口AuthenticationManager ,並在認證方法authenticate() 中將身份認證委托給具有認證資格的AuthenticationProvider 進行身份認證。
從上圖中我們可以看到AuthenticationManager的實現類有很多,至於為什么我只提及到ProviderManager,有時間的小伙伴可以進行源碼跟蹤就能發現。
ProviderManager的成員變量
- 關於AuthenticationEventPublisher不懂的小伙伴可以查看Security中的認證事件發布器 。
- providers存儲了一個 AuthenticationProvider 類型的list。和Security中的配置文件相對應。
- MessageSourceAccessor一個國際化消息來源訪問器,Security中用於信息提示。
AuthenticationProvider
- 接口認證類,定義了認證方法authenticate() 。
- AbstractUserDetailsAuthenticationProvider 為認證抽象類,實現了接口 AuthenticationProvider 定義的認證方法 authenticate()。還定義了抽象方法 retrieveUser() 用於查詢數據庫用戶信息,以及抽象方法 additionalAuthenticationChecks() 用作額外的身份驗證檢查。
DaoAuthenticationProvider
- 繼承自抽象類AbstractUserDetailsAuthenticationProvider,實現了該類的方法 retrieveUser() 和 additionalAuthenticationChecks()。
- DaoAuthenticationProvider 中還具有四個成員變量,分別是
- USER_NOT_FOUND_PASSWORD
顧名思義,該變量是與PasswordEncoder一同使用的,當Security未找到用戶時,用於PasswordEncoder.matches()執行的明文密碼,以防止惡意用戶確定用戶名是否有效的旁路攻擊的可能性。 - PasswordEncoder
密碼編碼器,Security中的主要作用是用於將明文密碼轉換成密文,它采用SHA-256算法,迭代1024次,使用一個密鑰(site-wide secret)以及8位隨機鹽對原密碼進行加密。 - userNotFoundEncodedPassword
同上方USER_NOT_FOUND_PASSWORD一致,只不過Security將其修飾為volatile的,確保了該變量不會因為編譯器的優化而被省略。
關於volatile關鍵字不懂的請查看volatile理解 - UserDetailsService
這個變量相信很多人都知道,不做過多的解釋,Security中用於查詢用戶詳細信息的接口 - UserDetailsPasswordService
顧名思義,該接口用於修改用戶的密碼,只有在使用持久存儲庫時才有效,基於內存的方式會拋異常。(用處不大,只做了解即可)
流程分析
static final class AuthenticationManagerDelegator implements AuthenticationManager {
private AuthenticationManagerBuilder delegateBuilder;
private AuthenticationManager delegate;
private final Object delegateMonitor = new Object();
AuthenticationManagerDelegator(AuthenticationManagerBuilder delegateBuilder) {
Assert.notNull(delegateBuilder, "delegateBuilder cannot be null");
this.delegateBuilder = delegateBuilder;
}
@Override
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
if (this.delegate != null) {
return this.delegate.authenticate(authentication);
}
synchronized (this.delegateMonitor) {
if (this.delegate == null) {
this.delegate = this.delegateBuilder.getObject();
this.delegateBuilder = null;
}
}
return this.delegate.authenticate(authentication);
}
@Override
public String toString() {
return "AuthenticationManagerDelegator [delegate=" + this.delegate + "]";
}
}
1、Security認證的入口為AuthenticationManager的authenticate()方法,從上面代碼中我們可以看出,AuthenticationManagerDelegator使用了單例模式來防止AuthenticationManager在初始化時發生無限遞歸,因此我們只分析上方的兩個實現類OAuth2AuthenticationManager和ProviderManager。
OAuth2AuthenticationManager
OAuth2AuthenticationManager的authenticate()的方法代碼如下:
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
if (authentication == null) {
throw new InvalidTokenException("Invalid token (token not found)");
}
(1) String token = (String) authentication.getPrincipal();
(2) OAuth2Authentication auth = tokenServices.loadAuthentication(token);
if (auth == null) {
throw new InvalidTokenException("Invalid token: " + token);
}
Collection<String> resourceIds = auth.getOAuth2Request().getResourceIds();
if (resourceId != null && resourceIds != null && !resourceIds.isEmpty() && !resourceIds.contains(resourceId)) {
throw new OAuth2AccessDeniedException("Invalid token does not contain resource id (" + resourceId + ")");
}
(3) checkClientDetails(auth);
if (authentication.getDetails() instanceof OAuth2AuthenticationDetails) {
OAuth2AuthenticationDetails details = (OAuth2AuthenticationDetails) authentication.getDetails();
// Guard against a cached copy of the same details
if (!details.equals(auth.getDetails())) {
// Preserve the authentication details from the one loaded by token services
details.setDecodedDetails(auth.getDetails());
}
}
auth.setDetails(authentication.getDetails());
auth.setAuthenticated(true);
return auth;
}
private void checkClientDetails(OAuth2Authentication auth) {
if (clientDetailsService != null) {
ClientDetails client;
try {
client = clientDetailsService.loadClientByClientId(auth.getOAuth2Request().getClientId());
}
catch (ClientRegistrationException e) {
throw new OAuth2AccessDeniedException("Invalid token contains invalid client id");
}
Set<String> allowed = client.getScope();
for (String scope : auth.getOAuth2Request().getScope()) {
if (!allowed.contains(scope)) {
throw new OAuth2AccessDeniedException(
"Invalid token contains disallowed scope (" + scope + ") for this client");
}
}
}
}
OAuth2AuthenticationManager用於集成了OAuth2.0時使用的,如果沒有用到,可以忽略。
其中:
- (1)處的代碼,期望傳入的身份驗證請求具有一個主體值,該主體值是一個訪問令牌值(一般在(authorization header)請求頭中)
- (2)處從ResourceServerTokenServices通過查詢數據庫中 oauth_client_details該表,加載身份驗證。
- 通過(3)處檢查資源id是否包含在授權請求中。檢查通過之后封裝OAuth2認證實體返回給UsernamePasswordAuthenticationFilter以確定認證成功或失敗。
ProviderManager
ProviderManager的authenticate()的方法代碼如下:
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
Class<? extends Authentication> toTest = authentication.getClass();
AuthenticationException lastException = null;
AuthenticationException parentException = null;
Authentication result = null;
Authentication parentResult = null;
boolean debug = logger.isDebugEnabled();
(1) for (AuthenticationProvider provider : getProviders()) {
if (!provider.supports(toTest)) {
continue;
}
if (debug) {
logger.debug("Authentication attempt using "
+ provider.getClass().getName());
}
try {
(2) result = provider.authenticate(authentication);
if (result != null) {
copyDetails(authentication, result);
break;
}
}
catch (AccountStatusException | InternalAuthenticationServiceException e) {
prepareException(e, authentication);
// SEC-546: Avoid polling additional providers if auth failure is due to
// invalid account status
throw e;
} catch (AuthenticationException e) {
lastException = e;
}
}
if (result == null && parent != null) {
// Allow the parent to try.
try {
result = parentResult = parent.authenticate(authentication);
}
catch (ProviderNotFoundException e) {
// ignore as we will throw below if no other exception occurred prior to
// calling parent and the parent
// may throw ProviderNotFound even though a provider in the child already
// handled the request
}
catch (AuthenticationException e) {
lastException = parentException = e;
}
}
if (result != null) {
if (eraseCredentialsAfterAuthentication
&& (result instanceof CredentialsContainer)) {
// Authentication is complete. Remove credentials and other secret data
// from authentication
((CredentialsContainer) result).eraseCredentials();
}
// If the parent AuthenticationManager was attempted and successful than it will publish an AuthenticationSuccessEvent
// This check prevents a duplicate AuthenticationSuccessEvent if the parent AuthenticationManager already published it
if (parentResult == null) {
eventPublisher.publishAuthenticationSuccess(result);
}
return result;
}
// Parent was null, or didn't authenticate (or throw an exception).
if (lastException == null) {
lastException = new ProviderNotFoundException(messages.getMessage(
"ProviderManager.providerNotFound",
new Object[] { toTest.getName() },
"No AuthenticationProvider found for {0}"));
}
// If the parent AuthenticationManager was attempted and failed than it will publish an AbstractAuthenticationFailureEvent
// This check prevents a duplicate AbstractAuthenticationFailureEvent if the parent AuthenticationManager already published it
if (parentException == null) {
prepareException(lastException, authentication);
}
throw lastException;
}
其中:
- (1)處的代碼從ProviderManager的屬性providers[List
] 中通過for循環拿到支持該類認證的AuthenticationProvider用於認證處理。 - (2)處的代碼,對用戶進行身份認證,認證過程如下所示。
在上面(2)處的代碼,使用了AbstractUserDetailsAuthenticationProvider的authenticate()方法,接下來具體分析該方法,代碼如下:
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication,
() -> messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.onlySupports",
"Only UsernamePasswordAuthenticationToken is supported"));
// Determine username
String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED"
: authentication.getName();
boolean cacheWasUsed = true;
UserDetails user = this.userCache.getUserFromCache(username);
if (user == null) {
cacheWasUsed = false;
try {
(1) user = retrieveUser(username,
(UsernamePasswordAuthenticationToken) authentication);
}
catch (UsernameNotFoundException notFound) {
logger.debug("User '" + username + "' not found");
if (hideUserNotFoundExceptions) {
throw new BadCredentialsException(messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.badCredentials",
"Bad credentials"));
}
else {
throw notFound;
}
}
Assert.notNull(user,
"retrieveUser returned null - a violation of the interface contract");
}
try {
preAuthenticationChecks.check(user);
(2) 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;
}
}
postAuthenticationChecks.check(user);
if (!cacheWasUsed) {
this.userCache.putUserInCache(user);
}
Object principalToReturn = user;
if (forcePrincipalAsString) {
principalToReturn = user.getUsername();
}
(3) return createSuccessAuthentication(principalToReturn, authentication, user);
}
-
(3)處創建一個成功的身份認證令牌並將用戶認證信息其放置到UsernamePasswordAuthenticationToken中。
查看源碼我們得知,AbstractUserDetailsAuthenticationProvider的(1)處和(2)處調用的方法沒有具體的實現,因此我們接下來分析它的子類DaoAuthenticationProvider
-
retrieveUser()
protected final UserDetails retrieveUser(String username,
UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException {
prepareTimingAttackProtection();
try {
(1) 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) {
mitigateAgainstTimingAttack(authentication);
throw ex;
}
catch (InternalAuthenticationServiceException ex) {
throw ex;
}
catch (Exception ex) {
throw new InternalAuthenticationServiceException(ex.getMessage(), ex);
}
}
(1)處調用DaoAuthenticationProvider成員變量UserDetailsService的方法loadUserByUsername()從數據庫中加載用戶詳細信息(用過Security的對此處應該是很熟悉了)
- additionalAuthenticationChecks()
protected void additionalAuthenticationChecks(UserDetails userDetails,
UsernamePasswordAuthenticationToken authentication)
throws AuthenticationException {
if (authentication.getCredentials() == null) {
logger.debug("Authentication failed: no credentials provided");
throw new BadCredentialsException(messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.badCredentials",
"Bad credentials"));
}
(1) String presentedPassword = authentication.getCredentials().toString();
(2) 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"));
}
}
(1)處從UsernamePasswordAuthenticationToken中調出了密碼,再由(2)處通過調用成員變量passwordEncoder對其密碼進行驗證。
以上就是AuthenticationManager的驗證大致流程,由於本人能力有限,如有錯誤,還請各位大佬多多包涵並在評論區進行留言指正,我會一一回復。