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的流程中了,具體認證程序就不再贅述了,
??以上就是整個認證流程,
??搞懂了認證流程,那么接下來如果想要自定義一些認證方式,就會非常容易了,比如定義多個資料源、添加登錄校驗碼等,下面,我們將通過兩個案例,來活學活用上面所講的認證流程,
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/500909.html
標籤:Java
上一篇:專案里的各種配置,你都了解嗎?
