SpringSecurity身份認證原理解析
Spring Security身份認證
- 用戶名和密碼被過濾器獲取到,封裝成 Authentication ,通常情況下是 UsernamePasswordAuthenticationToken 這個實現(xiàn)類。
- AuthenticationManager 身份管理器負責(zé)驗證這個 Authentication
- 認證成功后, AuthenticationManager 身份管理器返回一個被填充滿了信息的(包括上面提到的 權(quán)限信息,身份信息,細節(jié)信息,但密碼通常會被移除) Authentication 實例。
- SecurityContextHolder 安全上下文容器將第3步填充了信息的 Authentication ,通過 SecurityContextHolder.getContext().setAuthentication(…)方法,設(shè)置到其中。
public class AuthenticationExample { private static AuthenticationManager am = new SampleAuthenticationManager(); public static void main(String[] args) throws IOException { BufferedReader in = new BufferedReader(new InputStreamReader(System.in)); while (true) { System.out.println("please enter your username:"); String name = in.readLine(); System.out.println("please enter your password:"); String password = null; password = in.readLine(); try { // 封裝認證信息,未認證通過 UsernamePasswordAuthenticationToken request = new UsernamePasswordAuthenticationToken(name, password); // 認證邏輯 Authentication result = am.authenticate(request); //當(dāng)前線程綁定認證信息 SecurityContextHolder.getContext().setAuthentication(result); break; } catch (AuthenticationException e) { System.out.println("Authentication failed: " + e.getMessage()); } } } static class SampleAuthenticationManager implements AuthenticationManager { static final List<GrantedAuthority> AUTHORITIES = new ArrayList<GrantedAuthority>(); static { AUTHORITIES.add(new SimpleGrantedAuthority("ROLE_USER")); } @Override public Authentication authenticate(Authentication authentication) throws AuthenticationException { // 判斷條件,用戶名和密碼是否相同 if (authentication.getName().equals(authentication.getCredentials())){ return new UsernamePasswordAuthenticationToken(authentication.getName(),authentication.getCredentials(),AUTHORITIES); } throw new BadCredentialsException("Bad Credentials"); } } }
測試:
認證流程
SecurityFilterChain 過濾器鏈
Spring Security采用的是filterChain的設(shè)計方式,主要的功能大都由過濾器實現(xiàn),在啟動項目的時候,可以在日志中看到已有的過濾器,可在類似下面的日志里找到 DefaultSecurityFilterChain ,這里面則是SecurityFilterChain
2021-01-07 11:27:30.410 INFO 13880 --- [ main] o.s.s.web.DefaultSecurityFilterChain : Will secure any request with [org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter@153cd6bb, org.springframework.security.web.context.SecurityContextPersistenceFilter@71f0b72e, org.springframework.security.web.header.HeaderWriterFilter@aa149ed, org.springframework.security.web.authentication.logout.LogoutFilter@2de50ee4, org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter@151ef57f, org.springframework.security.web.session.ConcurrentSessionFilter@5c73f672, org.springframework.security.web.savedrequest.RequestCacheAwareFilter@2f508f3c, org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter@5eed2d86, org.springframework.security.web.authentication.rememberme.RememberMeAuthenticationFilter@36fc05ff, org.springframework.security.web.authentication.AnonymousAuthenticationFilter@61d84e08, org.springframework.security.web.session.SessionManagementFilter@31ff6309, org.springframework.security.web.access.ExceptionTranslationFilter@10fbbdb, org.springframework.security.web.access.intercept.FilterSecurityInterceptor@4e1459ea]
把各個過濾器抽取出來,我們可以看到是這樣,這也是過濾器鏈的先后順序。
- WebAsyncManagerIntegrationFilter
- SecurityContextPersistenceFilter
- HeaderWriterFilter
- LogoutFilter
- UsernamePasswordAuthenticationFilter
- JwtAuthorizationTokenFilter
- RequestCacheAwareFilter
- SecurityContextHolderAwareRequestFilter
- SessionManagementFilter
- ExceptionTranslationFilter
- FilterSecurityInterceptor
介紹幾個主要的作用
- SecurityContextPersistenceFilter
- Filter的入口和出口,它是用來將SecurityContext(認證的上下文,里面有登錄成功后的認證授權(quán)信息)對象持久到Session的Filter,同時會把SecurityContext設(shè)置給SecurityContextHolder方便我們獲取用戶認證授權(quán)信息
- UsernamePasswordAuthenticationFilter
- 默認攔截“/login”登錄請求,處理表單提交的登錄認證,將請求中的認證信息包括username,password等封裝成UsernamePasswordAuthenticationToken,然后調(diào)用AuthenticationManager的認證方法進行認證
- BasicAuthenticationFilter
- 基本認證,支持httpBasic認證方式的Filter
- RememberAuthenticationFilter
- 記住我功能實現(xiàn)的Filter
- AnonymousAuthenticationFilter
- 匿名Filter,用來處理匿名訪問的資源,如果用戶未登錄,SecurityContext中沒有Authentication,就會創(chuàng)建匿名的Token(AnonymousAuthenticationToken),然后通過SecurityContextHodler設(shè)置到SecurityContext中。
- ExceptionTranslationFilter
- 用來捕獲FilterChain所有的異常,進行處理,但是只會處理 AuthenticationException和AccessDeniedException,異常,其他的異常 會繼續(xù)拋出。
- FilterSecurityInterceptor
用來做授權(quán)的Filter,通過父類(AbstractSecurityInterceptor.beforeInvocation)調(diào)用AccessDecisionManager.decide方法對用戶進行授權(quán)。
UsernamePasswordAuthenticationFilter
UsernamePasswordAuthenticationFilter ,顧名思義,是用來處理用戶名密碼登錄的過濾器。所有的Filter核心方法都是 doFilter ,該過濾器的doFilter在其父抽象類中,過濾器只需實現(xiàn) attemptAuthentication 方法即可。
public class UsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter { // ~ Static fields/initializers // ===================================================================================== //從登錄請求中獲取參數(shù):username,password的名字 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; //默認支持POST登錄 private boolean postOnly = true; //默認攔截/login請求,Post方式 public UsernamePasswordAuthenticationFilter() { super(new AntPathRequestMatcher("/login", "POST")); } // ~ Methods // ======================================================================================================== public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { //判斷請求是否是POST 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(); //用戶名和密碼封裝Token UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken( username, password); //設(shè)置details屬性 // Allow subclasses to set the "details" property setDetails(request, authRequest); //調(diào)用AuthenticationManager().authenticate進行認證,參數(shù)就是Token對象 return this.getAuthenticationManager().authenticate(authRequest); }
AuthenticationManager
請求通過UsernamePasswordAuthenticationFilter調(diào)用AuthenticationManager,默認走的實現(xiàn)類是ProviderManager,它會找到能支持當(dāng)前認證的AuthenticationProvider實現(xiàn)類調(diào)用器authenticate方法執(zhí)行認證,認證成功后會清除密碼,然后拋出AuthenticationSuccessEvent事件
public class ProviderManager implements AuthenticationManager, MessageSourceAware, InitializingBean { ...省略... //這里authentication 是封裝了登錄請求的認證參數(shù), //即:UsernamePasswordAuthenticationFilter傳入的Token對象 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(); //找到所有的AuthenticationProvider ,選擇合適的進行認證 for (AuthenticationProvider provider : getProviders()) { //是否支持當(dāng)前認證 if (!provider.supports(toTest)) { continue; } ```java if (debug) { logger.debug("Authentication attempt using " + provider.getClass().getName()); } try { //調(diào)用provider執(zhí)行認證 result = provider.authenticate(authentication); if (result != null) { copyDetails(authentication, result); break; } } ...省略... } ...省略... //result就是Authentication ,使用的實現(xiàn)類依然是UsernamepasswordAuthenticationToken, //封裝了認證成功后的用戶的認證信息和授權(quán)信息 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) { //發(fā)布事件 eventPublisher.publishAuthenticationSuccess(result); } return result; }
DaoAuthenticationProvider
請求到達AuthenticationProvider,默認實現(xiàn)是DaoAuthenticationProvider,它的作用是根據(jù)傳入的Token中的username調(diào)用UserDetailService加載數(shù)據(jù)庫中的認證授權(quán)信息(UserDetails),然后使用PasswordEncoder對比用戶登錄密碼是否正確
public class DaoAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider { //密碼編碼器 private PasswordEncoder passwordEncoder; //UserDetailsService ,根據(jù)用戶名加載UserDetails對象,從數(shù)據(jù)庫加載的認證授權(quán)信息 private UserDetailsService userDetailsService; //認證檢查方法 protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException { if (authentication.getCredentials() == null) { logger.debug("Authentication failed: no credentials provided"); ```java throw new BadCredentialsException(messages.getMessage( "AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials")); } //獲取密碼 String presentedPassword = authentication.getCredentials().toString(); //通過passwordEncoder比較密碼,presentedPassword是用戶傳入的密碼,userDetails.getPassword()是從數(shù)據(jù)庫加載到的密碼 //passwordEncoder編碼器不一樣比較密碼的方式也不一樣 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")); } } //檢索用戶,參數(shù)為用戶名和Token對象 protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException { prepareTimingAttackProtection(); try { //調(diào)用UserDetailsService的loadUserByUsername方法, //根據(jù)用戶名檢索數(shù)據(jù)庫中的用戶,封裝成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); } } //創(chuàng)建認證成功的認證對象Authentication,使用的實現(xiàn)是UsernamepasswordAuthenticationToken, //封裝了認證成功后的認證信息和授權(quán)信息,以及賬戶的狀態(tài)等 @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); } ...省略...
這里提供了三個方法
- additionalAuthenticationChecks:通過passwordEncoder比對密碼
- retrieveUser:根據(jù)用戶名調(diào)用UserDetailsService加載用戶認證授權(quán)信息
- createSuccessAuthentication:登錄成功,創(chuàng)建認證對象Authentication
然而你發(fā)現(xiàn) DaoAuthenticationProvider 中并沒有authenticate認證方法,真正的認證邏輯是通過父類AbstractUserDetailsAuthenticationProvider.authenticate方法完成的
AbstractUserDetailsAuthenticationProvider
public abstract class AbstractUserDetailsAuthenticationProvider implements AuthenticationProvider, InitializingBean, MessageSourceAware { //認證邏輯 public Authentication authenticate(Authentication authentication) throws AuthenticationException { //得到傳入的用戶名 String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED" : authentication.getName(); //從緩存中得到UserDetails boolean cacheWasUsed = true; UserDetails user = this.userCache.getUserFromCache(username); if (user == null) { cacheWasUsed = false; ```java try { //檢索用戶,底層會調(diào)用UserDetailsService加載數(shù)據(jù)庫中的UserDetails對象,保護認證信息和授權(quán)信息 user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication); } catch (UsernameNotFoundException notFound) { ...省略... } try { //前置檢查,主要檢查賬戶是否鎖定,賬戶是否過期等 preAuthenticationChecks.check(user); //比對密碼在這個方法里面比對的 additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication); } catch (AuthenticationException exception) { ...省略... } //后置檢查 postAuthenticationChecks.check(user); if (!cacheWasUsed) { //設(shè)置UserDetails緩存 this.userCache.putUserInCache(user); } Object principalToReturn = user; if (forcePrincipalAsString) { principalToReturn = user.getUsername(); } //認證成功,創(chuàng)建Auhentication認證對象 return createSuccessAuthentication(principalToReturn, authentication, user); }
SecurityContextHolder
認證成功,請求會重新回到UsernamePasswordAuthenticationFilter,然后會通過其父類AbstractAuthenticationProcessingFilter.successfulAuthentication方法將認證對象封裝成SecurityContext設(shè)置到SecurityContextHolder中 protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException { ```java if (logger.isDebugEnabled()) { logger.debug("Authentication success. Updating SecurityContextHolder to contain: " + authResult); } //認證成功,吧Authentication 設(shè)置到SecurityContextHolder SecurityContextHolder.getContext().setAuthentication(authResult); //處理記住我業(yè)務(wù)邏輯 rememberMeServices.loginSuccess(request, response, authResult); // Fire event if (this.eventPublisher != null) { eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent( authResult, this.getClass())); } //重定向登錄成功地址 successHandler.onAuthenticationSuccess(request, response, authResult); }
然后后續(xù)請求又會回到SecurityContextPersistenceFilter,它就可以從SecurityContextHolder獲取到SecurityContext持久到SecurityContextRepository(默認實現(xiàn)是HttpSessionSecurityContextRepository基于Session存儲)
到此這篇關(guān)于SpringSecurity身份認證原理解析的文章就介紹到這了,更多相關(guān)SpringSecurity身份認證內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
java異步編程CompletableFuture使用示例詳解
這篇文章主要為大家介紹了java異步編程CompletableFuture使用示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2022-11-11SpringMVC JSON數(shù)據(jù)傳輸參數(shù)超詳細講解
有時候參數(shù)的傳遞還需要更多的參數(shù),比如一個獲取用戶信息的請求中既有用戶ID等基本參數(shù),還要求對查詢結(jié)果進行分頁,針對這種場景,一般都會將分頁參數(shù)封裝成一個對象,然后將它和基本參數(shù)一起傳給控制器2023-02-02SpringCloud實現(xiàn)基于RabbitMQ消息隊列的詳細步驟
在Spring Cloud框架中,我們可以利用RabbitMQ實現(xiàn)強大而可靠的消息隊列系統(tǒng),本篇將詳細介紹如何在Spring Cloud項目中集成RabbitMQ,并創(chuàng)建一個簡單的消息隊列,感興趣的朋友一起看看吧2024-03-03