1.認證流程分析 Spring Security中預設的一套登錄流程是非常完善並且嚴謹的。但是項目需求非常多樣化, 很多時候,我們可能還需要對Spring Secinity登錄流程進行定製,定製的前提是開發者先深刻理解Spring Security登錄流程,然後在此基礎之上,完成對登錄流程的定製。本 ...
1.認證流程分析
Spring Security中預設的一套登錄流程是非常完善並且嚴謹的。但是項目需求非常多樣化, 很多時候,我們可能還需要對Spring Secinity登錄流程進行定製,定製的前提是開發者先深刻理解Spring Security登錄流程,然後在此基礎之上,完成對登錄流程的定製。本文將從頭梳理 Spring Security登錄流程,並通過幾個常見的登錄定製案例,深刻地理解Spring Security登錄流程。
本章涉及的主要知識點有:
- 登錄流程分析。
- 配置多個數據源。
- 添加登錄驗證碼。
1.1登錄流程分析
要搞清楚Spring Security認證流程,我們得先認識與之相關的三個基本組件(Authentication 對象在前面文章種己經做過介紹,這裡不再贅述):AuthenticationManager、ProviderManager以及AuthenticationProvider,同時還要去瞭解接入認證功能的過濾器 AbstractAuthenticationProcessingFilter,這四個類搞明白了,基本上認證流程也就清楚了,下麵我們逐個分析一下。
1.1.1 AuthenticationManager
從名稱上可以看出,AuthenticationManager是一個認證管理器,它定義了 Spring Security 過濾器要如何執行認證操作。AuthenticationManager在認證成功後,會返回一個Authentication對象,這個Authentication對象會被設置到SecurityContextHolder中。如果開發者不想用Spring Security提供的一套認證機制,那麼也可以自定義認證流程,認證成功後,手動將Authentication 存入 SecurityContextHolder 中。
public interface AuthenticationManager {
Authentication authenticate(Authentication var1) throws AuthenticationException;
}
從 AuthenticationManager 的源碼中可以看到,AuthenticationManager 對傳入的 Authentication對象進行身份認證,此時傳入的Authentication參數只有用戶名/密碼等簡單的屬性,如果認證成功,返回的Authentication的屬性會得到完全填充,包括用戶所具備的角色信息。AuthenticationManager是一個介面,它有著諸多的實現類,開發者也可以自定義 AuthenticationManager的實現類,不過在實際應用中,我們使用最多的是ProviderManager,在 Spring S ecurity 框架中,預設也是使用 ProviderManager。
1.1.2 AuthenticationProvider
Spring Security支持多種不同的認證方式,不同的認證方式對應不同的身份 類型,AuthenticationProvider就是針對不同的身份類型執行具體的身份認證。例如,常見的 DaoAuthenticationProvider 用來支持用戶名/密碼登錄認證,RememberMeAuthenticationProvider 用來支持“記住我”的認證。
public interface AuthenticationProvider {
Authentication authenticate(Authentication var1) throws AuthenticationException;
boolean supports(Class<?> var1);
}
- authenticate方法用來執行具體的身份憂證。
- supports方法用來判斷當前AuthenticationProvider是否支持對應的身份類型。
當使用用戶名/密碼的方式登錄時,對應的AuthenticationProvider實現類是 DaoAuthenticationProvider , 而 DaoAuthenticationProvider 繼承自 AbstractUserDetailsAuthenticationProvider並且沒有重寫authenticate方法,所以具體的認證邏輯在AbstractUserDetailsAuthenticationProvider 的 authenticate 方法中。我們就從 AbstractUserDetailsAuthenticationProvider開始看起:
查看代碼
public abstract class AbstractUserDetailsAuthenticationProvider implements AuthenticationProvider, InitializingBean, MessageSourceAware {
protected final Log logger = LogFactory.getLog(this.getClass());
protected MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor();
private UserCache userCache = new NullUserCache();
private boolean forcePrincipalAsString = false;
protected boolean hideUserNotFoundExceptions = true;
private UserDetailsChecker preAuthenticationChecks = new AbstractUserDetailsAuthenticationProvider.DefaultPreAuthenticationChecks();
private UserDetailsChecker postAuthenticationChecks = new AbstractUserDetailsAuthenticationProvider.DefaultPostAuthenticationChecks();
private GrantedAuthoritiesMapper authoritiesMapper = new NullAuthoritiesMapper();
public AbstractUserDetailsAuthenticationProvider() {
}
protected abstract void additionalAuthenticationChecks(UserDetails var1, UsernamePasswordAuthenticationToken var2) throws AuthenticationException;
public final void afterPropertiesSet() throws Exception {
Assert.notNull(this.userCache, "A user cache must be set");
Assert.notNull(this.messages, "A message source must be set");
this.doAfterPropertiesSet();
}
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication, () -> {
return this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.onlySupports", "Only UsernamePasswordAuthenticationToken is supported");
});
String username = authentication.getPrincipal() == null ? "NONE_PROVIDED" : authentication.getName();
boolean cacheWasUsed = true;
UserDetails user = this.userCache.getUserFromCache(username);
if (user == null) {
cacheWasUsed = false;
try {
user = this.retrieveUser(username, (UsernamePasswordAuthenticationToken)authentication);
} catch (UsernameNotFoundException var6) {
this.logger.debug("User '" + username + "' not found");
if (this.hideUserNotFoundExceptions) {
throw new BadCredentialsException(this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
}
throw var6;
}
Assert.notNull(user, "retrieveUser returned null - a violation of the interface contract");
}
try {
this.preAuthenticationChecks.check(user);
this.additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken)authentication);
} catch (AuthenticationException var7) {
if (!cacheWasUsed) {
throw var7;
}
cacheWasUsed = false;
user = this.retrieveUser(username, (UsernamePasswordAuthenticationToken)authentication);
this.preAuthenticationChecks.check(user);
this.additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken)authentication);
}
this.postAuthenticationChecks.check(user);
if (!cacheWasUsed) {
this.userCache.putUserInCache(user);
}
Object principalToReturn = user;
if (this.forcePrincipalAsString) {
principalToReturn = user.getUsername();
}
return this.createSuccessAuthentication(principalToReturn, authentication, user);
}
protected Authentication createSuccessAuthentication(Object principal, Authentication authentication, UserDetails user) {
UsernamePasswordAuthenticationToken result = new UsernamePasswordAuthenticationToken(principal, authentication.getCredentials(), this.authoritiesMapper.mapAuthorities(user.getAuthorities()));
result.setDetails(authentication.getDetails());
return result;
}
protected void doAfterPropertiesSet() throws Exception {
}
public UserCache getUserCache() {
return this.userCache;
}
public boolean isForcePrincipalAsString() {
return this.forcePrincipalAsString;
}
public boolean isHideUserNotFoundExceptions() {
return this.hideUserNotFoundExceptions;
}
protected abstract UserDetails retrieveUser(String var1, UsernamePasswordAuthenticationToken var2) throws AuthenticationException;
public void setForcePrincipalAsString(boolean forcePrincipalAsString) {
this.forcePrincipalAsString = forcePrincipalAsString;
}
public void setHideUserNotFoundExceptions(boolean hideUserNotFoundExceptions) {
this.hideUserNotFoundExceptions = hideUserNotFoundExceptions;
}
public void setMessageSource(MessageSource messageSource) {
this.messages = new MessageSourceAccessor(messageSource);
}
public void setUserCache(UserCache userCache) {
this.userCache = userCache;
}
public boolean supports(Class<?> authentication) {
return UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication);
}
protected UserDetailsChecker getPreAuthenticationChecks() {
return this.preAuthenticationChecks;
}
public void setPreAuthenticationChecks(UserDetailsChecker preAuthenticationChecks) {
this.preAuthenticationChecks = preAuthenticationChecks;
}
protected UserDetailsChecker getPostAuthenticationChecks() {
return this.postAuthenticationChecks;
}
public void setPostAuthenticationChecks(UserDetailsChecker postAuthenticationChecks) {
this.postAuthenticationChecks = postAuthenticationChecks;
}
public void setAuthoritiesMapper(GrantedAuthoritiesMapper authoritiesMapper) {
this.authoritiesMapper = authoritiesMapper;
}
private class DefaultPostAuthenticationChecks implements UserDetailsChecker {
private DefaultPostAuthenticationChecks() {
}
public void check(UserDetails user) {
if (!user.isCredentialsNonExpired()) {
AbstractUserDetailsAuthenticationProvider.this.logger.debug("User account credentials have expired");
throw new CredentialsExpiredException(AbstractUserDetailsAuthenticationProvider.this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.credentialsExpired", "User credentials have expired"));
}
}
}
private class DefaultPreAuthenticationChecks implements UserDetailsChecker {
private DefaultPreAuthenticationChecks() {
}
public void check(UserDetails user) {
if (!user.isAccountNonLocked()) {
AbstractUserDetailsAuthenticationProvider.this.logger.debug("User account is locked");
throw new LockedException(AbstractUserDetailsAuthenticationProvider.this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.locked", "User account is locked"));
} else if (!user.isEnabled()) {
AbstractUserDetailsAuthenticationProvider.this.logger.debug("User account is disabled");
throw new DisabledException(AbstractUserDetailsAuthenticationProvider.this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.disabled", "User is disabled"));
} else if (!user.isAccountNonExpired()) {
AbstractUserDetailsAuthenticationProvider.this.logger.debug("User account is expired");
throw new AccountExpiredException(AbstractUserDetailsAuthenticationProvider.this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.expired", "User account has expired"));
}
}
}
}
- 一開始先聲明一個用戶緩存對象userCache,預設情況下沒有啟用緩存對象。
- hideUserNotFoundExceptions表示是否隱藏用戶名查找失敗的異常,預設為true,為了確保系統安全,用戶在登錄失敗時只會給出一個模糊提示,例如“用戶名或密碼輸入錯誤“ 在Spring Security內部,如果用戶名查找失敗,則會拋出UsernameNotFoundException異常, 但是該異常會被自動隱藏,轉而通過一個BadCredentialsException異常來代替它,這樣,開發者在處理登錄失敗異常時,無論是用戶名輸入錯誤還是密碼輸入錯誤,收到的總是 BadCredentialsException,這樣做的一個好處是可以避免新手程式員將用戶名輸入錯誤和密碼輸入錯誤兩個異常分開提示。
- forcePrincipalAsString表示是否強制將Principal對象當成字元串來處理,預設是falser,Authentication中的Principal屬性類型是一個Object,正常來說,通過Principal屬性可以獲取到當前登錄用戶對象(即UserDetails),但是如果forcePrincipalAsString設置為true,則 Authentication中的Principal屬性返回就是當前登錄用戶名,而不是用戶對象。
- preAuthenticationChecks對象則是用於做用戶狀態檢査,在用戶認證過程中,需要檢驗用戶狀態是否正常,例如賬戶是否被鎖定、賬戶是否可用、賬戶是否過期等。
- postAuthenticationChecks對象主要負責在密碼校驗成功後,檢査密碼是否過期。
- additionalAuthenticationChecks是一個抽象方法,主要就是校驗密碼,具體的實現在 DaoAuthenticationProvider 中。
- authenticate方法就是核心的校驗方法了。在方法中,首先從登錄數據中獲取用戶名, 然後根據用戶名去緩存中查詢用戶對象,如果査詢不到,則根據用戶名調用retrieveUser方法從資料庫中載入用戶;如果沒有載入到用戶,則拋出異常(用戶不存在異常會被隱藏)。拿到用戶對象之後,首先調用check方法進行用戶狀態檢査,然後調用 additionalAuthenticationChecks 方法進行密碼的校驗操作,最後調用 postAuthenticationChecks.check方法檢査密碼是否過期,當所有步驟都順利完成後,調用createSuccessAuthentication 方法創建一個認證後的 UsernamePasswordAuthenticationToken 對象並返回,認證後的對象中包含了認證主體、憑證以及角色等信息。
這就是 AbstractUserDetailsAuthenticationProvider類的工作流程,有幾個抽象方法是在 DaoAuthenticationProvider 中實現的,我們再來看一下 DaoAuthenticationProvider中的定義:
查看代碼
public class DaoAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {
private static final String USER_NOT_FOUND_PASSWORD = "userNotFoundPassword";
private PasswordEncoder passwordEncoder;
private volatile String userNotFoundEncodedPassword;
private UserDetailsService userDetailsService;
private UserDetailsPasswordService userDetailsPasswordService;
public DaoAuthenticationProvider() {
this.setPasswordEncoder(PasswordEncoderFactories.createDelegatingPasswordEncoder());
}
protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
if (authentication.getCredentials() == null) {
this.logger.debug("Authentication failed: no credentials provided");
throw new BadCredentialsException(this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
} else {
String presentedPassword = authentication.getCredentials().toString();
if (!this.passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
this.logger.debug("Authentication failed: password does not match stored value");
throw new BadCredentialsException(this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
}
}
}
protected void doAfterPropertiesSet() {
Assert.notNull(this.userDetailsService, "A UserDetailsService must be set");
}
protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
this.prepareTimingAttackProtection();
try {
UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
if (loadedUser == null) {
throw new InternalAuthenticationServiceException("UserDetailsService returned null, which is an interface contract violation");
} else {
return loadedUser;
}
} catch (UsernameNotFoundException var4) {
this.mitigateAgainstTimingAttack(authentication);
throw var4;
} catch (InternalAuthenticationServiceException var5) {
throw var5;
} catch (Exception var6) {
throw new InternalAuthenticationServiceException(var6.getMessage(), var6);
}
}
protected Authentication createSuccessAuthentication(Object principal, Authentication authentication, UserDetails user) {
boolean upgradeEncoding = this.userDetailsPasswordService != null && this.passwordEncoder.upgradeEncoding(user.getPassword());
if (upgradeEncoding) {
String presentedPassword = authentication.getCredentials().toString();
String newPassword = this.passwordEncoder.encode(presentedPassword);
user = this.userDetailsPasswordService.updatePassword(user, newPassword);
}
return super.createSuccessAuthentication(principal, authentication, user);
}
private void prepareTimingAttackProtection() {
if (this.userNotFoundEncodedPassword == null) {
this.userNotFoundEncodedPassword = this.passwordEncoder.encode("userNotFoundPassword");
}
}
private void mitigateAgainstTimingAttack(UsernamePasswordAuthenticationToken authentication) {
if (authentication.getCredentials() != null) {
String presentedPassword = authentication.getCredentials().toString();
this.passwordEncoder.matches(presentedPassword, this.userNotFoundEncodedPassword);
}
}
public void setPasswordEncoder(PasswordEncoder passwordEncoder) {
Assert.notNull(passwordEncoder, "passwordEncoder cannot be null");
this.passwordEncoder = passwordEncoder;
this.userNotFoundEncodedPassword = null;
}
protected PasswordEncoder getPasswordEncoder() {
return this.passwordEncoder;
}
public void setUserDetailsService(UserDetailsService userDetailsService) {
this.userDetailsService = userDetailsService;
}
protected UserDetailsService getUserDetailsService() {
return this.userDetailsService;
}
public void setUserDetailsPasswordService(UserDetailsPasswordService userDetailsPasswordService) {
this.userDetailsPasswordService = userDetailsPasswordService;
}
}
- 首先定義了 USER_NOT_FOUND_PASSWORD常量,這個是當用戶查找失敗時的預設密碼;passwordEncoder是一個密碼加密和比對工具,這個在後面會有專門的介紹,這裡先不做過多解釋;userNotFoundEncodedPassword變數則用來保存預設密碼加密後的值; userDetailsService是一個用戶查找工具,userDetailsService在前面己經講過,這裡不再贅述; userDetailsPasswordService則用來提供密碼修改服務。
- 在DaoAuthenticationProvider的構造方法中,預設就會指定PasswordEncoder,當然開發者也可以通過set方法自定義PasswordEncoder。
- additionalAuthenticationchecks方法主要進行密碼校驗,該方法第一個參數userDetails 是從資料庫中查詢出來的用戶對象,第二個參數authentication則是登錄用戶輸入的參數。從這兩個參數中分別提取出來用戶密碼,然後調用passwordEncoder.matches方法進行密碼比對。
- retrieveUser方法則是獲取用戶對象的方法,具體做法就是調用 UserDetailsService#loadUserByUsername 方法去資料庫中查詢。
- 在retrieveUser方法中,有一個值得關註的地方。在該方法一開始,首先會調用 prepareTimingAttackProtection 方法,該方法的作用是使用 PasswordEncoder 對常量 USER_NOT_FOUND_PASSWORD 進行加密,將加密結果保存在 userNotFoundEncodedPassword變數中。當根據用戶名查找用戶時,如果拋出了 UsernameNotFoundException異常, 則調用mitigateAgainstTimingAttack方法進行密碼比對由有讀者會說,用戶都沒查找到,怎麼 比對密碼?需要註意,在調用mitigateAgainstTimingAttack方法進行密碼比對時,使用了 userNotFoundEncodedPassword變數作為預設密碼和登錄請求傳來的用戶密碼進行比對,這是 一個一開始就註定要失敗的密碼比對,那麼為什麼還要進行比對呢?這主要是為了避免旁道攻擊(Side-channel attack)。如果根據用戶名査找用戶失敗,就直接拋出異常而不進行密碼比對, 那麼黑客經過大量的測試,就會發現有的請求耗費時間明顯小於其他請求,那麼進而可以得出該請求的用戶名是一個不存在的用戶名(因為用戶名不存在,所以不需要密碼比對,進而節省時間),這樣就可以獲取到系統信息。為了避免這一問題,所以當用戶查找失敗時,也會調用 mitigateAgainstTimingAttack方法進行密碼比對,這樣就可以迷惑黑客。
- createSuccessAuthentication方法則是在登錄成功後,創建一個全新的 UsernamePasswordAuthenticationToken對象,同時會判斷是否需要進行密碼升級,如果需要進行密碼升級,就會在該方法中進行加密方案升級。通過對 AbstractUserDetailsAuthenticationProvider 和 DaoAuthenticationProvider 的講解,相信你己經很明白AuthenticationProvider中的認證邏輯了。
在密碼學中,旁道攻擊(Side-channel attack )又稱側通道攻擊、邊通道攻擊。這種攻擊方式不是暴力破解或者是研究加密演算法的弱點。它是基於從密碼系統的物理實現中獲取信息, 比如時間、功率消耗、電磁泄漏等,這些信息可被利用於對系統的進一步破解。
1.1.3 ProviderManager
ProviderManager是AuthenticationManager的一個重要實現類,正在開始學習之前,我們先通過一幅圖來瞭解一下ProviderManager和AuthenticationProvider之間的關係,如圖3-1所示。
圖 3-1
在Spring Security中,由於系統可能同時支持多種不同的認證方式,例如同時支持用戶名 /密碼認證、RememberMe認證、手機號碼動態認證等,而不同的認證方式對應了不同的 AuthenticationProvider,所以一個完整的認證流程可能由多個AuthenticationProvider來提供。
多個AuthenticationProvider將組成一個列表,這個列表將由ProviderManager代理。換句話說,在 ProviderManager 中存在一個 AuthenticationProvider 列表,在 ProviderManager 中遍歷列表中的每一個AuthenticationProvider去執行身份認證,最終得到認證結果。
ProviderManager 本身也可以再配置一個 AuthenticationManager 作為 parent,這樣當 ProviderManager認證失敗之後,就可以進入到parent中再次進行認證。理論上來說, ProviderManager的parent可以是任意類型的 AuthenticationManager,但是通常都是由 ProviderManager 來扮演 parent 的角色,也就是 ProviderManager 是 ProviderManager 的 parent。
ProviderManager本身也可以有多個,多個ProviderManager共用同一個parent,當存在多個過濾器鏈的時候非常有用。當存在多個過濾器鏈時,不同的路徑可能對應不同的認證方式, 但是不同路徑可能又會同時存在一些共有的認證方式,這些共有的認證方式可以在parent中統 一處理。
根據上面的介紹,圖 3-2是ProviderManager和AuthenticationProvider關係圖。
圖 3-2
我們重點看一下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();
Iterator var8 = this.getProviders().iterator();
while(var8.hasNext()) {
AuthenticationProvider provider = (AuthenticationProvider)var8.next();
if (provider.supports(toTest)) {
if (debug) {
logger.debug("Authentication attempt using " + provider.getClass().getName());
}
try {
result = provider.authenticate(authentication);
if (result != null) {
this.copyDetails(authentication, result);
break;
}
} catch (InternalAuthenticationServiceException | AccountStatusException var13) {
this.prepareException(var13, authentication);
throw var13;
} catch (AuthenticationException var14) {
lastException = var14;
}
}
}
if (result == null && this.parent != null) {
try {
result = parentResult = this.parent.authenticate(authentication);
} catch (ProviderNotFoundException var11) {
} catch (AuthenticationException var12) {
parentException = var12;
lastException = var12;
}
}
if (result != null) {
if (this.eraseCredentialsAfterAuthentication && result instanceof CredentialsContainer) {
((CredentialsContainer)result).eraseCredentials();
}
if (parentResult == null) {
this.eventPublisher.publishAuthenticationSuccess(result);
}
return result;
} else {
if (lastException == null) {
lastException = new ProviderNotFoundException(this.messages.getMessage("ProviderManager.providerNotFound", new Object[]{toTest.getName()}, "No AuthenticationProvider found for {0}"));
}
if (parentException == null) {
this.prepareException((AuthenticationException)lastException, authentication);
}
throw lastException;
}
}
這段源碼的邏輯還是非常清晰的,我們分析一下:
- 首先獲取authentication對象的類型。
- 分別定義當前認證過程拋出的異常、parent中認證時拋出的異常、當前認證結果以及parent中認證結果對應的變數。
- getProviders 方法用來獲取當前 ProviderManager 所代理的所有 AuthenticationProvider 對象,遍歷這些AuthenticationProvider對象進行身份認證。
- 判斷當前AuthenticationProvider是否支持當前Authentication對象,要是不支持,則繼續處理列表中的下一個AuthenticationProvider對象
- 調用provider.authenticate方法進行身份認證,如果認證成功,返回認證後的 Authentication對象,同時調用copyDetails方法給Authentication對象的details屬性賦值,由於可能是多個AuthenticationProvider執行認證操作,所以如果拋出異常,則通過lastException 變數來記錄。
- 在for迴圈執行完成後,如果result還是沒有值,說明所有的AuthenticationProvider 都認證失敗,此時如果parent不為空,則調用parent的authenticate方法進行認證。
- 接下來,如果result不為空,就將result中的憑證擦除,防止泄漏,如果使用了用戶名/密碼的方式登錄,那麼所謂的擦除實際上就是將密碼欄位設置為null,同時將登錄成功的事件發佈出去(發佈登錄成功事件需要parentResult為null。如果parentResult不為null,表示在parent中已經認證成功了,認證成功的事件也己經在parent中發佈出去了,這樣會導致認證成功的事件重覆發佈)。如果用戶認證成功,此時就將result返回,後面的代碼也就不再執行了。
- 如果前面沒能返回result,說明認證失敗。如果lastException為null,說明parent為 null或者沒有認證亦或者認證失敗了但是沒有拋出異常,此時構造ProviderNotFoundException 異常賦值給lastException。
- 如果parentException為null,發佈認證失敗事件(如果parentException不為null, 則說明認證失敗事件已經發佈過了)。
- 最後拋出lastException異常。
這就是ProviderManager中authenticate方法的身份認證邏輯,其他方法的源碼要相對簡單很多,在這裡就不一一解釋了,
現在,大家已經熟悉了 Authentication、AuthenticationManager、AuthenticationProvider 以 及ProviderManager的工作原理了,接下來的問題就是這些組件如何跟登錄關聯起來?這就涉及一個重要的過濾器----------------------- AbstractAuthenticationProcessingFilter。
1.1.4 AbstractAuthenticationProcessingFilter
作為 Spring Security 過濾器鏈中的一環,AbstractAuthenticationProcessingFilter可以用來處理任何提交給它的身份認證,圖3-3描述了 AbstractAuthenticationProcessingFilter的工作流程:
圖 3-3
圖中顯示的流程是一個通用的架構。
AbstractAuthenticationProcessingFilter作為一個抽象類,如果使用用戶名/密碼的方式登錄, 那麼它對應的實現類是 UsernamePasswordAuthenticationFilter;構造出來的 Authentication 對象則是 UsernamePasswordAuthenticationToken。至於 AuthenticationManager,前面已經說過,一 般情況下它的實現類就是ProviderManager,這裡在ProviderManager中進行認證,認證成功就會進入認證成功的回調,否則進入認證失敗的回調。因此,我們可以對上面的流程圖再做進一 步細化,如圖3-4所示。
圖 3-4
前面第2章中所涉及的認證流程基本上就是這樣,我們來大致梳理一下:
- 當用戶提交登錄請求時,UsernamePasswordAuthenticationFilter會從當前請求 HttpServletRequest中提取出登錄用戶名/密碼,然後創建出一個 UsernamePasswordAuthenticationToken 對象。
- UsernamePasswordAuthenticationToken 對象將被傳入 ProviderManager 中進行具體的認證操作。
- 如果認證失敗,則SecurityContextHolder中相關信息將被清除,登錄失敗回調也會被調用,
- 如果認證成功,則會進行登錄信息存儲、Session併發處理、登錄成功事件發佈以及登錄成功方法回調等操作。
這是認證的一個大致流程。接下來我們結合 AbstractAuthenticationProcessingFilter和 UsernamePasswordAuthenticationFilter的源碼來看一下。
先來看 AbstractAuthenticationProcessingFilter源碼(部分核心代碼):
查看代碼
public abstract class AbstractAuthenticationProcessingFilter extends GenericFilterBean
implements ApplicationEventPublisherAware, MessageSourceAware {
protected ApplicationEventPublisher eventPublisher;
protected AuthenticationDetailsSource<HttpServletRequest, ?> authenticationDetailsSource = new WebAuthenticationDetailsSource();
private AuthenticationManager authenticationManager;
protected MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor();
private RememberMeServices rememberMeServices = new NullRememberMeServices();
private RequestMatcher requiresAuthenticationRequestMatcher;
private boolean continueChainBeforeSuccessfulAuthentication = false;
private SessionAuthenticationStrategy sessionStrategy = new NullAuthenticatedSessionStrategy();
private boolean allowSessionCreation = true;
private AuthenticationSuccessHandler successHandler = new SavedRequestAwareAuthenticationSuccessHandler();
private AuthenticationFailureHandler failureHandler = new SimpleUrlAuthenticationFailureHandler();
protected AbstractAuthenticationProcessingFilter(String defaultFilterProcessesUrl) {
setFilterProcessesUrl(defaultFilterProcessesUrl);
}
protected AbstractAuthenticationProcessingFilter(
RequestMatcher requiresAuthenticationRequestMatcher) {
Assert.notNull(requiresAuthenticationRequestMatcher,
"requiresAuthenticationRequestMatcher cannot be null");
this.requiresAuthenticationRequestMatcher = requiresAuthenticationRequestMatcher;
}
@Override
public void afterPropertiesSet() {
Assert.notNull(authenticationManager, "authenticationManager must be specified");
}
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
if (!requiresAuthentication(request, response)) {
chain.doFilter(request, response);
return;
}
if (logger.isDebugEnabled()) {
logger.debug("Request is to process authentication");
}
Authentication authResult;
try {
authResult = attemptAuthentication(request, response);
if (authResult == null) {
return;
}
sessionStrategy.onAuthentication(authResult, request, response);
}
catch (InternalAuthenticationServiceException failed) {
logger.error(
"An internal error occurred while trying to authenticate the user.",
failed);
unsuccessfulAuthentication(request, response, failed);
return;
}
catch (AuthenticationException failed) {
unsuccessfulAuthentication(request, response, failed);
return;
}
if (continueChainBeforeSuccessfulAuthentication) {
chain.doFilter(request, response);
}
successfulAuthentication(request, response, chain, authResult);
}
protected boolean requiresAuthentication(HttpServletRequest request,
HttpServletResponse response) {
return requiresAuthenticationRequestMatcher.matches(request);
}
public abstract Authentication attemptAuthentication(HttpServletRequest request,
HttpServletResponse response) throws AuthenticationException, IOException,
ServletException;
protected void successfulAuthentication(HttpServletRequest request,
HttpServletResponse response, FilterChain chain, Authentication authResult)
throws IOException, ServletException {
if (logger.isDebugEnabled()) {
logger.debug("Authentication success. Updating SecurityContextHolder to contain: "
+ authResult);
}
SecurityContextHolder.getContext().setAuthentication(authResult);
rememberMeServices.loginSuccess(request, response, authResult);
if (this.eventPublisher != null) {
eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(
authResult, this.getClass()));
}
successHandler.onAuthenticationSuccess(request, response, authResult);
}
protected void unsuccessfulAuthentication(HttpServletRequest request,
HttpServletResponse response, AuthenticationException failed)
throws IOException, ServletException {
SecurityContextHolder.clearContext();
if (logger.isDebugEnabled()) {
logger.debug("Authentication request failed: " + failed.toString(), failed);
logger.debug("Updated SecurityContextHolder to contain null Authentication");
logger.debug("Delegating to authentication failure handler " + failureHandler);
}
rememberMeServices.loginFail(request, response);
failureHandler.onAuthenticationFailure(request, response, failed);
}
}
- 首先通過requiresAuthentication方法來判斷當前請求是不是登錄認證請求,如果是認證請求,就執行接下來的認證代碼;如果不是認證請求,則直接繼續走剩餘的過濾器即可。
- 調用attemptAuthentication方法來獲取一個經過認證後的Authentication對象, attemptAuthentication方法是一個抽象方法,具體實現在它的子類 UsernamePasswordAuthenticationFilter 中。
- 認證成功後,通過sessionStrategy.onAuthentication方法來處理session併發問題。
- continueChainBeforeSuccessfulAuthentication變數用來判斷請求是否還需要繼續向下走,預設情況下該參數的值為false,即認證成功後,後續的過濾器將不再執行了。
- unsuccessfulAuthentication方法用來處理認證失敗事宜,主要做了三件事:①從 SecurityContextHolder中清除數據;②清除Cookie等信息;③調用認證失敗的回調方法。
- successfulAuthentication方法主要用來處理認證成功事宜,主要做了四件事:①向 SecurityContextHolder中存入用戶信息;②處理Cookie;③發佈認證成功事件,這個事件類型 InteractiveAuthenticationSuccessEvent,表示通過一些自動交互的方式認證成功,例如通過 RememberMe的方式登錄;④調用認證成功的回調方法。
這就是 AbstractAuthenticationProcessingFilter大致上所做的事情,還有一個抽象方法 attemptAuthentication 是在它的繼承類 UsernamePasswordAuthenticationFilter中實現的,接下來我們來看—下UsernamePasswordAuthenticationFilter類:
查看代碼
public class UsernamePasswordAuthenticationFilter extends
AbstractAuthenticationProcessingFilter {
public static final String SPRING_SECURITY_FORM_USERNAME_KEY = "username";
public static final String SPRING_SECURITY_FORM_PASSWORD_KEY = "password";
private String usernameParameter = SPRING_SECURITY_FORM_USERNAME_KEY;
private String passwordParameter = SPRING_SECURITY_FORM_PASSWORD_KEY;
private boolean postOnly = true;
public UsernamePasswordAuthenticationFilter() {
super(new AntPathRequestMatcher("/login", "POST"));
}
public Authentication attemptAuthentication(HttpServletRequest request,
HttpServletResponse response) throws AuthenticationException {
if (postOnly && !request.getMethod().equals("POST")) {
throw new AuthenticationServiceException(
"Authentication method not supported: " + request.getMethod());
}
String username = obtainUsername(request);
String password = obtainPassword(request);
if (username == null) {
username = "";
}
if (password == null) {
password = "";
}
username = username.trim();
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
username, password);
setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
}
@Nullable
protected String obtainPassword(HttpServletRequest request) {
return request.getParameter(passwordParameter);
}
@Nullable
protected String obtainUsername(HttpServletRequest request) {
return request.getParameter(usernameParameter);
}
}
- 首先聲明瞭預設情況下登錄表單的用戶名欄位和密碼欄位,用戶名欄位的key預設是username,密碼欄位的key預設是password。當然,這兩個欄位也可以自定義,自定義的方式就是我們在 SecurityConfig 中配置的 .usernameParameter("uname")和 .passwordParameter("passwd")(參考前幾節)
- 在UsernamePasswordAuthenticationFilter過濾器構建的時候,指定了當前過濾器只用來處理登錄請求,預設的登錄請求是/login,當然開發者也可以自行配置。
- 接下來就是最重要的attemptAuthentication方法了,在該方法中,首先確認請求是 post類型;然後通過obtainUsername和obtainPassword方法分別從請求中提取出用戶名和密碼, 具體的提取過程就是調用request.getParameter方法;拿到登錄請求傳來的用戶名/密碼之後, 構造出一個 authRequest,然後調用 getAuthenticationManager().authenticate 方法進行認證,這就進入到我們前面所說的ProviderManager的流程中了,具體認證過程就不再贅述了。
以上就是整個認證流程。
搞懂了認證流程,那麼接下來如果想要自定義一些認證方式,就會非常容易了,比如定義多個數據源、添加登錄校驗碼等。下麵,我們將通過兩個案例,來活學活用上面所講的認證流程。