Security 登錄認證流程詳細分析詳解
最近在寫畢業設計的時候用這個框架,小夥伴給我提瞭個
多種登錄方式
的需求,說僅僅隻有賬號、密碼登錄不太行,說讓我增加幾種方式,如:手機短信驗證登錄、郵箱驗證登錄、第三方登錄等等(前兩個已經實現,第三方登錄還沒搞定)一開始也挺讓人懵逼,無從下手的。看瞭好幾篇博客,都弄的不完整,或者就是太高級瞭,我不太能行。之後就是看博客,說弄懂原理、流程後,寫多種方式其實也蠻簡單。然後我就老老實實的去Debug瞭。
這樣子的效果是十分好的,多Debug幾回,無論是對使用,還是對於編寫代碼,以及對這個技術的理解都會加深一些,以前一些迷惑也會恍然大悟。
Debug的過程要找到一個脈絡,不要心急,前期多做個筆記,不會多查一下,那樣一切都會顯得非常輕松的。
前文:👉SpringBoot整合Security,實現權限控制
本文適合需要入門及已經會簡單使用Security的小夥伴們。
對於一門技術,會使用是說明我們對它已經有瞭一個簡單瞭解,把脈絡都掌握清楚,我們才能更好的使用它,以及更好的實現定制化。
接下來就讓😀來帶大傢一起看看吧。
Security如何處理表單提交賬號和密碼,以及保存用戶身份信息的。
如有不足之處,請大傢批評指正。
一、🍟前言:流程圖:
二、🍤前臺發送請求
用戶向/login
接口使用POST
方式提交用戶名、密碼。/login
是沒指定時默認的接口
三、請求到達UsernamePasswordAuthenticationFilter過濾器
請求首先會來到:👉UsernamePasswordAuthenticationFilter
/** UsernamePasswordAuthenticationFilter:處理身份驗證表單提交 以及將請求信息封裝為Authentication 然後返回給上層父類, 父類再通過 SecurityContextHolder.getContext().setAuthentication(authResult); 將驗證過的Authentication 保存至安全上下文中 */ 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 static final AntPathRequestMatcher DEFAULT_ANT_PATH_REQUEST_MATCHER = new AntPathRequestMatcher("/login", "POST"); //可以通過對應的set方法修改 private String usernameParameter = SPRING_SECURITY_FORM_USERNAME_KEY; private String passwordParameter = SPRING_SECURITY_FORM_PASSWORD_KEY; private boolean postOnly = true; // 初始化一個用戶密碼 認證過濾器 默認的登錄uri 是 /login 請求方式是POST public UsernamePasswordAuthenticationFilter() { super(DEFAULT_ANT_PATH_REQUEST_MATCHER); } public UsernamePasswordAuthenticationFilter(AuthenticationManager authenticationManager) { super(DEFAULT_ANT_PATH_REQUEST_MATCHER, authenticationManager); } @Override public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { if (this.postOnly && !request.getMethod().equals("POST")) { throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod()); } String username = obtainUsername(request); username = (username != null) ? username : ""; username = username.trim(); String password = obtainPassword(request); password = (password != null) ? password : ""; //把賬號名、密碼封裝到一個認證Token對象中,這是一個通行證,但是此時的狀態時不可信的,通過認證後才會變為可信的 UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password); // Allow subclasses to set the "details" property //記錄遠程地址,如果會話已經存在(它不會創建),還將設置會話 ID setDetails(request, authRequest); //使用 父類中的 AuthenticationManager 對Token 進行認證 return this.getAuthenticationManager().authenticate(authRequest); } /** obtainUsername和obtainPassword就是方便從request中獲取到username和password 實際上如果在前後端分離的項目中 我們大都用不上😂 因為前端傳過來的是JSON數據,我們通常是使用JSON工具類進行解析 */ @Nullable protected String obtainPassword(HttpServletRequest request) { return request.getParameter(this.passwordParameter); } @Nullable protected String obtainUsername(HttpServletRequest request) { return request.getParameter(this.usernameParameter); } /** 提供以便子類可以配置放入身份驗證請求的詳細信息 */ protected void setDetails(HttpServletRequest request, UsernamePasswordAuthenticationToken authRequest) { authRequest.setDetails(this.authenticationDetailsSource.buildDetails(request)); } /** ...省略一些不重要的代碼 set get */ }
四、🍹制作UsernamePasswordAuthenticationToken
將獲取到的數據制作成一個令牌UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
之前我們在圖中講瞭我們實際封裝的是一個Authentication
對象,UsernamePasswordAuthenticationToken
是一個默認實現類。
我們簡單看一下他們的結構圖:
public class UsernamePasswordAuthenticationToken extends AbstractAuthenticationToken { private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID; // 這裡就是用戶名和密碼 自定義時 根據自己需求進行重寫 private final Object principal; private Object credentials; /** //把賬號名、密碼封裝到一個認證UsernamePasswordAuthenticationToken對象中,這是一個通行證,但是此時的狀態時不可信的, //我們在這也可以看到 權限是null, setAuthenticated(false);是表示此刻身份是未驗證的 所以此時狀態是不可信的 */ public UsernamePasswordAuthenticationToken(Object principal, Object credentials) { super(null); this.principal = principal; this.credentials = credentials; setAuthenticated(false); } /** 這個時候才是可信的狀態 */ 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 對Token 進行認證
AuthenticationManager是身份認證器,認證的核心接口
我們繼續對return this.getAuthenticationManager().authenticate(authRequest);
進行分析.
//我們可以看到 AuthenticationManager 實際上就是一個接口,所以它並不做真正的事情,隻是提供瞭一個標準,我們就繼續去看看它的實現類,看看是誰幫它做瞭事。 public interface AuthenticationManager { //嘗試對傳遞的Authentication對象進行身份Authentication ,如果成功則返回完全填充的Authentication對象(包括授予的權限)。 Authentication authenticate(Authentication authentication) throws AuthenticationException; }
六、我們找到瞭AuthenticationManager 實現類ProviderManager
我們找到ProviderManager
實現瞭AuthenticationManager
。(但是你會發現它也不做事,又交給瞭別人做😂)
ProviderManager
並不是自己直接對請求進行驗證,而是將其委派給一個 AuthenticationProvider
列表。列表中的每一個 AuthenticationProvider
將會被依次查詢是否需要通過其進行驗證,每個 provider的驗證結果隻有兩個情況:拋出一個異常或者完全填充一個 Authentication
對象的所有屬性。
在這個閱讀中,我刪除瞭許多雜七雜八的代碼,一些判斷,異常處理,我都去掉瞭,隻針對最重要的那幾個看。
public class ProviderManager implements AuthenticationManager, MessageSourceAware, InitializingBean { //省略瞭一些代碼 private List<AuthenticationProvider> providers = Collections.emptyList(); /** * 嘗試對傳遞的Authentication對象進行身份Authentication 。AuthenticationProvider的列表將被連續嘗試, * 直到AuthenticationProvider表明它能夠驗證所傳遞的Authentication對象的類型。 然後將嘗試使用該AuthenticationProvider 。 */ @Override public Authentication authenticate(Authentication authentication) throws AuthenticationException { Class<? extends Authentication> toTest = authentication.getClass(); Authentication result = null; Authentication parentResult = null; int currentPosition = 0; int size = this.providers.size(); //我們遍歷AuthenticationProvider 列表中每個Provider依次進行認證 // 不過你會發現 AuthenticationProvider 也是一個接口,它的實現類才是真正做事的人 ,下文有 for (AuthenticationProvider provider : getProviders()) { if (!provider.supports(toTest)) { continue; } //... try { //provider.authenticate() //參數:身份驗證 - 身份驗證請求對象。 //返回:一個完全經過身份驗證的對象,包括憑據。 如果AuthenticationProvider無法支持對傳遞的Authentication對象進行身份驗證,則可能返回null ,我們接著看它的實現類是什麼樣子的 result = provider.authenticate(authentication); if (result != null) { copyDetails(authentication, result); break; } } catch (AccountStatusException | InternalAuthenticationServiceException ex) { //.... } } // 如果 AuthenticationProvider 列表中的Provider都認證失敗,且之前有構造一個 AuthenticationManager 實現類,那麼利用AuthenticationManager 實現類 繼續認證 if (result == null && this.parent != null) { // Allow the parent to try. try { parentResult = this.parent.authenticate(authentication); result = parentResult; } catch (ProviderNotFoundException ex) { // ... } } //認證成功 if (result != null) { if (eraseCredentialsAfterAuthentication && (result instanceof CredentialsContainer)) { // Authentication is complete. Remove credentials and other secret data // from authentication //成功認證後刪除驗證信息 ((CredentialsContainer) result).eraseCredentials(); } //發佈登錄成功事件 eventPublisher.publishAuthenticationSuccess(result); return result; } // 沒有認證成功,拋出異常 if (lastException == null) { lastException = new ProviderNotFoundException(messages.getMessage( "ProviderManager.providerNotFound", new Object[] { toTest.getName() }, "No AuthenticationProvider found for {0}")); } prepareException(lastException, authentication); throw lastException; } }
七、🍦AuthenticationProvider接口
public interface AuthenticationProvider { /** 認證方法 參數:身份驗證 - 身份驗證請求對象。 返回:一個完全經過身份驗證的對象,包括憑據。 */ Authentication authenticate(Authentication authentication) throws AuthenticationException; /** 該Provider是否支持對應的Authentication 如果此AuthenticationProvider支持指定的Authentication對象,則返回true 。 */ boolean supports(Class<?> authentication); }
註意
:boolean supports(Class<?> authentication);
方式上完整JavaDoc的註釋是:
如果有多個
AuthenticationProvider
都支持同一個Authentication 對象,那麼第一個 能夠成功驗證Authentication的 Provder 將填充其屬性並返回結果,從而覆蓋早期支持的AuthenticationProvider
拋出的任何可能的AuthenticationException
。一旦成功驗證後,將不會嘗試後續的AuthenticationProvider
。如果所有的AuthenticationProvider
都沒有成功驗證Authentication
,那麼將拋出最後一個Provider拋出的AuthenticationException
。(AuthenticationProvider
可以在Spring Security配置類中配置)
機譯不是很好理解,我們翻譯成通俗易懂點:
當然有時候我們有多個不同的
AuthenticationProvider
,它們分別支持不同的Authentication
對象,那麼當一個具體的AuthenticationProvier
傳進入ProviderManager
的內部時,就會在AuthenticationProvider
列表中挑選其對應支持的provider對相應的 Authentication對象進行驗證
這個知識和實現多種登錄方式相關聯,我簡單的說一下我的理解。
我們這裡講解的是默認的登錄方式,用到的是UsernamePasswordAuthenticationFilter和UsernamePasswordAuthenticationToken以及後文中的DaoAuthenticationProvider
這些,來進行身份的驗證,但是如果我們後期需要添加手機短信驗證碼登錄或者郵件驗證碼或者第三方登錄等等。
那麼我們也會重新繼承AbstractAuthenticationProcessingFilter、AbstractAuthenticationToken、AuthenticationProvider
進行重寫,因為不同的登錄方式認證邏輯是不一樣的,AuthenticationProvider
也會不一樣,我們使用用戶名和密碼登錄,Security 提供瞭一個 AuthenticationProvider
的簡單實現 DaoAuthenticationProvider
,它使用瞭一個 UserDetailsService
來查詢用戶名、密碼和 GrantedAuthority
,實際使用中我們都會實現UserDetailsService
接口,從數據庫中查詢相關用戶信息,AuthenticationProvider
的認證核心就是加載對應的 UserDetails
來檢查用戶輸入的密碼是否與其匹配。
流程圖大致如下:
八、🍭DaoAuthenticationProvider
AuthenticationProvider
它的實現類、繼承類很多,我們直接看和User
相關的,會先找到AbstractUserDetailsAuthenticationProvider
這個抽象類。
我們先看看這個抽象類,然後再看它的實現類,看他們是如何一步一步遞進的。
/** 一個基本的AuthenticationProvider ,它允許子類覆蓋和使用UserDetails對象。 該類旨在響應UsernamePasswordAuthenticationToken身份驗證請求。 驗證成功後,將創建UsernamePasswordAuthenticationToken並將其返回給調用者。 令牌將包括用戶名的String表示或從身份驗證存儲庫返回的UserDetails作為其主體。 */ public abstract class AbstractUserDetailsAuthenticationProvider implements AuthenticationProvider, InitializingBean, MessageSourceAware { //...省略瞭一些代碼 private UserCache userCache = new NullUserCache(); private GrantedAuthoritiesMapper authoritiesMapper = new NullAuthoritiesMapper(); //認證方法 @Override public Authentication authenticate(Authentication authentication) throws AuthenticationException { Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication, () -> this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.onlySupports", "Only UsernamePasswordAuthenticationToken is supported")); //判斷用戶名是否為空 String username = determineUsername(authentication); boolean cacheWasUsed = true; //先查緩存 UserDetails user = this.userCache.getUserFromCache(username); if (user == null) { cacheWasUsed = false; try { user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication); } catch (UsernameNotFoundException ex) { this.logger.debug("Failed to find user '" + username + "'"); if (!this.hideUserNotFoundExceptions) { throw ex; } throw new BadCredentialsException(this.messages .getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials")); } Assert.notNull(user, "retrieveUser returned null - a violation of the interface contract"); } try { //一些檢查 this.preAuthenticationChecks.check(user); additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication); } catch (AuthenticationException ex) { if (!cacheWasUsed) { throw ex; } // There was a problem, so try again after checking // we're using latest data (i.e. not from the cache) cacheWasUsed = false; //retrieveUser 是個沒有抽象的方法 稍後我們看看它的實現類是如何實現的 user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication); //一些檢查信息 用戶是否可用什麼的 this.preAuthenticationChecks.check(user); additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication); } this.postAuthenticationChecks.check(user); if (!cacheWasUsed) { this.userCache.putUserInCache(user); } Object principalToReturn = user; if (this.forcePrincipalAsString) { principalToReturn = user.getUsername(); } //創建一個成功的Authentication對象。 return createSuccessAuthentication(principalToReturn, authentication, user); } private String determineUsername(Authentication authentication) { return (authentication.getPrincipal() == null) ? "NONE_PROVIDED" : authentication.getName(); } /** 創建一個成功的Authentication對象。 這個也允許字類進行實現。 如果要給密碼加密的話,一般字類都會重新進行實現 */ 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()); this.logger.debug("Authenticated user"); return result; } /** 允許子類從特定於實現的位置實際檢索UserDetails ,如果提供的憑據不正確,則可以選擇立即拋出AuthenticationException (如果需要以用戶身份綁定到資源以獲得或生成一個UserDetails ) */ protected abstract UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException; //... }
DaoAuthenticationProvider
:真正做事情的人
/** 從UserDetailsService檢索用戶詳細信息的AuthenticationProvider實現。 */ public class DaoAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider { // ...省略瞭一些代碼 /** */ @Override protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException { prepareTimingAttackProtection(); try { //UserDetailsService簡單說就是加載對應的UserDetails的接口(一般從數據庫),而UserDetails包含瞭更詳細的用戶信息 //通過loadUserByUsername獲取用戶信息 ,返回一個 UserDetails 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); } } // 重新父類的方法,對密碼進行一些加密操作 @Override 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); } //... }
九、🍣UserDetailsService和UserDetails接口
UserDetailsService
簡單說就是定義瞭一個加載對應的UserDetails
的接口,我們在使用中,大都數都會實現這個接口,從數據庫中查詢相關的用戶信息。
//加載用戶特定數據的核心接口。 public interface UserDetailsService { //根據用戶名定位用戶 UserDetails loadUserByUsername(String username) throws UsernameNotFoundException; }
UserDetails
也是一個接口,實際開發中,同樣對它也會進行實現,進行定制化的使用。
/** 提供核心用戶信息。 出於安全目的,Spring Security 不直接使用實現。 它們隻是存儲用戶信息,然後將這些信息封裝到Authentication對象中。 這允許將非安全相關的用戶信息(例如電子郵件地址、電話號碼等)存儲在方便的位置。 */ public interface UserDetails extends Serializable { //返回授予用戶的權限。 Collection<? extends GrantedAuthority> getAuthorities(); String getPassword(); String getUsername(); //指示用戶的帳戶是否已過期。 無法驗證過期帳戶 boolean isAccountNonExpired(); //指示用戶是被鎖定還是未鎖定。 無法對鎖定的用戶進行身份驗證。 boolean isAccountNonLocked(); //指示用戶的憑據(密碼)是否已過期。 過期的憑據會阻止身份驗證。 boolean isCredentialsNonExpired(); //指示用戶是啟用還是禁用。 無法對禁用的用戶進行身份驗證。 boolean isEnabled(); }
10、🍻返回過程
1、DaoAuthenticationProvider
類下UserDetails retrieveUser()
方法中通過this.getUserDetailsService().loadUserByUsername(username);
獲取到用戶信息後;
2、將UserDetails
返回給父類AbstractUserDetailsAuthenticationProvider
中的調用處(即Authentication authenticate(Authentication authentication)
方法中)
3、AbstractUserDetailsAuthenticationProvider
拿到返回的UserDetails
後,最後返回給調用者的是return createSuccessAuthentication(principalToReturn, authentication, user);
這裡就是創建瞭一個可信的 UsernamePasswordAuthenticationToken
,即身份憑證。
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()); this.logger.debug("Authenticated user"); return result; }
4、我們再回到ProviderManager
的Authentication authenticate(Authentication authentication)
方法中的調用處,這個時候我們的用戶信息已經是驗證過的,我們接著向上層調用處返回。
5、回到UsernamePasswordAuthenticationFilter
中的return this.getAuthenticationManager().authenticate(authRequest);
語句中,這個時候還得繼續向上層返回
6、返回到AbstractAuthenticationProcessingFilter
中,我們直接按ctrl+b
看是誰調用瞭它。
public abstract class AbstractAuthenticationProcessingFilter extends GenericFilterBean implements ApplicationEventPublisherAware, MessageSourceAware { @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { doFilter((HttpServletRequest) request, (HttpServletResponse) response, chain); } private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException { if (!requiresAuthentication(request, response)) { chain.doFilter(request, response); return; } try { // 這裡就是調用處。 Authentication authenticationResult = attemptAuthentication(request, response); if (authenticationResult == null) { // return immediately as subclass has indicated that it hasn't completed return; } // session相關,這裡我們不深聊 //發生新的身份驗證時執行與 Http 會話相關的功能。 this.sessionStrategy.onAuthentication(authenticationResult, request, response); // Authentication success if (this.continueChainBeforeSuccessfulAuthentication) { chain.doFilter(request, response); } //看方法名我們就知道 這是我們需要的拉 //成功驗證省份後調用 successfulAuthentication(request, response, chain, authenticationResult); } catch (InternalAuthenticationServiceException failed) { this.logger.error("An internal error occurred while trying to authenticate the user.", failed); //驗證失敗調用 unsuccessfulAuthentication(request, response, failed); } catch (AuthenticationException ex) { // Authentication failed //驗證失敗調用 unsuccessfulAuthentication(request, response, ex); } } }
//成功身份驗證的默認行為。 //1、在SecurityContextHolder上設置成功的Authentication對象 //2、通知配置的RememberMeServices登錄成功 //3、通過配置的ApplicationEventPublisher觸發InteractiveAuthenticationSuccessEvent //4、將附加行為委托給AuthenticationSuccessHandler 。 //子類可以覆蓋此方法以在身份驗證成功後繼續FilterChain 。 protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException { //將通過驗證的Authentication保存至安全上下文 SecurityContextHolder.getContext().setAuthentication(authResult); if (this.logger.isDebugEnabled()) { this.logger.debug(LogMessage.format("Set SecurityContextHolder to %s", authResult)); } this.rememberMeServices.loginSuccess(request, response, authResult); if (this.eventPublisher != null) { this.eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(authResult, this.getClass())); } this.successHandler.onAuthenticationSuccess(request, response, authResult); }
其實不管是驗證成功調用或是失敗調用,大都數我們在實際使用中,都是需要重寫的,返回我們自己想要返回給前端的數據。
到此這篇關於Security 登錄認證流程詳細分析詳解的文章就介紹到這瞭,更多相關Security 登錄認證內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!
推薦閱讀:
- Spring Security認證的完整流程記錄
- Spring Security 實現用戶名密碼登錄流程源碼詳解
- Spring Security登陸流程講解
- Spring Security 實現多種登錄方式(常規方式外的郵件、手機驗證碼登錄)
- Spring Security OAuth 自定義授權方式實現手機驗證碼