1. 程式人生 > >【認證與授權】Spring Security系列之認證流程解析

【認證與授權】Spring Security系列之認證流程解析

> 上面我們一起開始了Spring Security的初體驗,並通過簡單的配置甚至零配置就可以完成一個簡單的認證流程。可能我們都有很大的疑惑,這中間到底發生了什麼,為什麼簡單的配置就可以完成一個認證流程啊,可我啥都沒看見,沒有寫頁面,沒有寫介面。這一篇我們將深入到原始碼層面一起來了解一下spring security到底是怎麼工作的。 ### 準備工作 在開始原始碼理解前,我們先來做一項基本的準備工作,從日誌中去發現線索,因為我們發現什麼都沒有配置的情況下,他也可以正常的工作,並給我們預置了一個臨時的使用者user。那麼他肯定是在工程啟動的時候做了什麼事情,上一篇我們也提到了是如果生成user使用者和密碼的。這篇我們將仔細的去了解一下。 1、*首先我們配置在`applicaiton.yml`中調整一下日誌級別* ```yml logging: level: org.springframework.security: debug ``` 我們將`security`相關的日誌打印出來,一起來啟動或者執行的時候到底發生了什麼。 2、*啟動`spring-security-basic` 工程* ![](https://i.loli.net/2020/04/12/bohgYFAzqynPB2e.gif) !!!找到了 ### 日誌過濾 ``` (1) Eagerly initializing {org.springframework.boot.autoconfigure.security.servlet.WebSecurityEnablerConfiguration=org.springframework.boot.autoconfigure.security.servlet.WebSecurityEnablerConfiguration@52e04737} (2) Using default configure(HttpSecurity). If subclassed this will potentially override subclass configure(HttpSecurity). (3) Adding web access control expression 'authenticated', for any request (4) Validated configuration attributes ``` ### 逐個解析 #### 1、`WebSecurityEnablerConfiguration` 告訴我們它初始化了一個配置類`WebSecurityEnablerConfiguration` 不管!找到原始碼再說 ```java @Configuration( proxyBeanMethods = false ) @ConditionalOnBean({WebSecurityConfigurerAdapter.class}) @ConditionalOnMissingBean( name = {"springSecurityFilterChain"} ) @ConditionalOnWebApplication( type = Type.SERVLET ) @EnableWebSecurity public class WebSecurityEnablerConfiguration { public WebSecurityEnablerConfiguration() { } } ``` ???怎麼只有這麼一點東西,這個類為什麼會在初始化的時候啟動?這裡簡單的指出來 首先找到`spring-boot-autoconfigure-版本.jar`下的`META-INF/spring.factorites`檔案,其中有這樣一段 ``` org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration,\ org.springframework.boot.autoconfigure.security.servlet.UserDetailsServiceAutoConfiguration,\ org.springframework.boot.autoconfigure.security.servlet.SecurityFilterAutoConfiguration,\ ``` 我們可以暫時不去深究這是什麼意思,總之,在`springboot`啟動的時候,會將這裡配置走一遍(後期可能也會寫一點關於`springboot`啟動原理的文章...)我們一個一個來看一下 ##### 1.1 `SecurityAutoConfiguration` ```java @Configuration( proxyBeanMethods = false ) @ConditionalOnClass({DefaultAuthenticationEventPublisher.class}) @EnableConfigurationProperties({SecurityProperties.class}) @Import({SpringBootWebSecurityConfiguration.class, WebSecurityEnablerConfiguration.class, SecurityDataConfiguration.class}) public class SecurityAutoConfiguration { public SecurityAutoConfiguration() { } @Bean @ConditionalOnMissingBean({AuthenticationEventPublisher.class}) public DefaultAuthenticationEventPublisher authenticationEventPublisher(ApplicationEventPublisher publisher) { return new DefaultAuthenticationEventPublisher(publisher); } } ``` 在這個類中我們重點關注 ``` @EnableConfigurationProperties({SecurityProperties.class}) @Import({SpringBootWebSecurityConfiguration.class, WebSecurityEnablerConfiguration.class, SecurityDataConfiguration.class}) ``` 首先是`SecurityProperties` ```java @ConfigurationProperties( // A 字首 prefix = "spring.security" ) public class SecurityProperties { // .. private SecurityProperties.User user = new SecurityProperties.User(); // ... public static class User { // 預設指定一個 private String name = "user"; // 預設隨機密碼 private String password = UUID.randomUUID().toString(); private List roles = new ArrayList(); // 預設密碼是系統生成的(重點關注一下) private boolean passwordGenerated = true; // ... public void setPassword(String password) { // 如果指定了自定義了密碼,那就false 並覆蓋password if (StringUtils.hasLength(password)) { this.passwordGenerated = false; this.password = password; } } //..... } // ..... } ``` 篇幅問題這裡我刪除了很多程式碼。直接看裡面的註釋就好了,這也就是為什麼我們不配置任何資訊,也有一個預設的使用者,以及我們用配置資訊覆蓋了預設使用者的關鍵資訊所在。 其次是`@Import`註解,這個其實就是xml配置方式中的標籤
引入另外的配置,這裡引入了`SpringBootWebSecurityConfiguration` `WebSecurityEnablerConfiguration` `SecurityDataConfiguration` ``` @Configuration( proxyBeanMethods = false ) @ConditionalOnClass({WebSecurityConfigurerAdapter.class}) @ConditionalOnMissingBean({WebSecurityConfigurerAdapter.class}) @ConditionalOnWebApplication( type = Type.SERVLET ) public class SpringBootWebSecurityConfiguration { public SpringBootWebSecurityConfiguration() { } @Configuration( proxyBeanMethods = false ) // 其實也沒幹啥,就是一個空的物件,什麼也沒覆蓋 @Order(2147483642) static class DefaultConfigurerAdapter extends WebSecurityConfigurerAdapter { DefaultConfigurerAdapter() { } } } ``` 他們指向了一個關鍵的配置`@ConditionalOnBean({WebSecurityConfigurerAdapter.class})` 需要`WebSecurityConfigurerAdapter`才會進行載入,那這個關鍵的類是什麼時候載入的呢?這就回到了我們在日誌中發現的第一個載入的類資訊``WebSecurityEnablerConfiguration`` 上面有個一非常關鍵的註解`@EnableWebSecurity` 瞧瞧幹了啥 ``` @Retention(value = java.lang.annotation.RetentionPolicy.RUNTIME) @Target(value = { java.lang.annotation.ElementType.TYPE }) @Documented // 引入了配置類 WebSecurityConfiguration @Import({ WebSecurityConfiguration.class, SpringWebMvcImportSelector.class, OAuth2ImportSelector.class }) @EnableGlobalAuthentication @Configuration public @interface EnableWebSecurity { /** * Controls debugging support for Spring Security. Default is false. * @return if true, enables debug support with Spring Security */ boolean debug() default false; } ``` ##### 1.2 `WebSecurityConfiguration` 原來,首先他是個配置註解,也`import`了`WebSecurityConfiguration` ```java @Configuration(proxyBeanMethods = false) public class WebSecurityConfiguration implements ImportAware, BeanClassLoaderAware { // 1、宣告一個 webSecurity 一起來看一下他是什麼時候初始化的 private WebSecurity webSecurity; // 2、是否為除錯模式 private Boolean debugEnabled; private List> webSecurityConfigurers; private ClassLoader beanClassLoader; // 3、關鍵點,後置物件處理器,用來初始化物件 @Autowired(required = false) private ObjectPostProcessor objectObjectPostProcessor; @Bean(name = AbstractSecurityWebApplicationInitializer.DEFAULT_FILTER_NAME) public Filter springSecurityFilterChain() throws Exception { boolean hasConfigurers = webSecurityConfigurers != null && !webSecurityConfigurers.isEmpty(); // 6 、如果每沒初始化,直接指定獲取物件 WebSecurityConfigurerAdapter if (!hasConfigurers) { WebSecurityConfigurerAdapter adapter = objectObjectPostProcessor .postProcess(new WebSecurityConfigurerAdapter() { }); webSecurity.apply(adapter); } // 7、 開始構建物件 webSecurity return webSecurity.build(); } // 4、通過setter方式注入 webSecurityConfigurers @Autowired(required = false) public void setFilterChainProxySecurityConfigurer( ObjectPostProcessor objectPostProcessor, // 獲取 0 步中獲取到的物件資訊 @Value("#{@autowiredWebSecurityConfigurersIgnoreParents.getWebSecurityConfigurers()}") List> webSecurityConfigurers) throws Exception { // 5、 這裡通過後置物件處理器來進行 webSecurity 的初始化 webSecurity = objectPostProcessor.postProcess(new WebSecurity(objectPostProcessor)); if (debugEnabled != null) { webSecurity.debug(debugEnabled); } webSecurityConfigurers.sort(AnnotationAwareOrderComparator.INSTANCE); Integer previousOrder = null; Object previousConfig = null; for (SecurityConfigurer config : webSecurityConfigurers) { Integer order = AnnotationAwareOrderComparator.lookupOrder(config); if (previousOrder != null && previousOrder.equals(order)) { throw new IllegalStateException( "@Order on WebSecurityConfigurers must be unique. Order of " + order + " was already used on " + previousConfig + ", so it cannot be used on " + config + " too."); } previousOrder = order; previousConfig = config; } for (SecurityConfigurer webSecurityConfigurer : webSecurityConfigurers) { // 放入到 AbstractConfiguredSecurityBuilder 的配置集合中 webSecurity.apply(webSecurityConfigurer); } this.webSecurityConfigurers = webSecurityConfigurers; } // 0 先自動織入webSecurityConfigurers // 關鍵點就是獲取 beanFactory.getBeansOfType(WebSecurityConfigurer.class); @Bean public static AutowiredWebSecurityConfigurersIgnoreParents autowiredWebSecurityConfigurersIgnoreParents( ConfigurableListableBeanFactory beanFactory) { return new AutowiredWebSecurityConfigurersIgnoreParents(beanFactory); } } ``` 上面我們已經看到了步驟7,通常情況下都會去`build` ```java public abstract class AbstractSecurityBuilder implements SecurityBuilder { private AtomicBoolean building = new AtomicBoolean(); private O object; public final O build() throws Exception { if (this.building.compareAndSet(false, true)) { // 這裡呼叫doBuild的最終方法 this.object = doBuild(); return this.object; } throw new AlreadyBuiltException("This object has already been built"); } public final O getObject() { if (!this.building.get()) { throw new IllegalStateException("This object has not been built"); } return this.object; } // 這裡是抽象方法,直接找到其唯一的子類 AbstractConfiguredSecurityBuilder protected abstract O doBuild() throws Exception; } ``` ```java @Override protected final O doBuild() throws Exception { synchronized (configurers) { buildState = BuildState.INITIALIZING; // 前置檢查 beforeInit(); // 初始化 init(); buildState = BuildState.CONFIGURING; beforeConfigure(); configure(); buildState = BuildState.BUILDING; O result = performBuild(); buildState = BuildState.BUILT; return result; } } ``` 不知不覺我們已經找到了`spring`中的關鍵方法`init`了,很多時候我們在定義介面是都會有一個`init`方法來定義注入時呼叫 前面我們知道 `SpringBootWebSecurityConfiguration `初始化了一個物件,同時也通過`AutowiredWebSecurityConfigurersIgnoreParents`拿到了`WebSecurityConfigurerAdapter `的子類 `DefaultConfigurerAdapter`,現在開始`init()`,其實就是開始`WebSecurityConfigurerAdapter`的`init()`方法。說了這裡可能有的同學就會比較熟悉了,這就是關鍵配置的介面卡類。 程式碼稍後貼出來,暫時先不看,到這裡為止,我們才梳理了`springboot`自動配置中的`SecurityAutoConfiguration` 下面我們才開始第二個類 #### 2、 `UserDetailsServiceAutoConfiguration` ```java @Configuration( proxyBeanMethods = false ) @ConditionalOnClass({AuthenticationManager.class}) @ConditionalOnBean({ObjectPostProcessor.class}) @ConditionalOnMissingBean( value = {AuthenticationManager.class, AuthenticationProvider.class, UserDetailsService.class}, type = {"org.springframework.security.oauth2.jwt.JwtDecoder", "org.springframework.security.oauth2.server.resource.introspection.OpaqueTokenIntrospector"} ) public class UserDetailsServiceAutoConfiguration { private static final String NOOP_PASSWORD_PREFIX = "{noop}"; private static final Pattern PASSWORD_ALGORITHM_PATTERN = Pattern.compile("^\\{.+}.*$"); private static final Log logger = LogFactory.getLog(UserDetailsServiceAutoConfiguration.class); public UserDetailsServiceAutoConfiguration() { } @Bean @ConditionalOnMissingBean( type = {"org.springframework.security.oauth2.client.registration.ClientRegistrationRepository"} ) // 這裡載入了從配置檔案或者預設生成的使用者資訊,以及加密方法 @Lazy public InMemoryUserDetailsManager inMemoryUserDetailsManager(SecurityProperties properties, ObjectProvider passwordEncoder) { User user = properties.getUser(); List roles = user.getRoles(); return new InMemoryUserDetailsManager(new UserDetails[]{org.springframework.security.core.userdetails.User.withUsername(user.getName()).password(this.getOrDeducePassword(user, (PasswordEncoder)passwordEncoder.getIfAvailable())).roles(StringUtils.toStringArray(roles)).build()}); } private String getOrDeducePassword(User user, PasswordEncoder encoder) { String password = user.getPassword(); if (user.isPasswordGenerated()) { logger.info(String.format("%n%nUsing generated security password: %s%n", user.getPassword())); } return encoder == null && !PASSWORD_ALGORITHM_PATTERN.matcher(password).matches() ? "{noop}" + password : password; } } ``` *這裡也出現了一個`info`日誌,當我們使用預設`user`使用者時,密碼會從這裡列印在控制檯* 這個配置類的關鍵就是生成一個預設的`InMemoryUserDetailsManager`物件。 #### 4、`SecurityFilterAutoConfiguration` 這個類就不詳細介紹了,就是註冊一些過濾器。 ------ 回到`WebSecurityConfigurerAdapter` 這個介面卡類,我們關注基本的`init()`方法,其他的都是一些預設的配置 ```java public void init(final WebSecurity web) throws Exception { final HttpSecurity http = getHttp(); web.addSecurityFilterChainBuilder(http).postBuildAction(() -> { FilterSecurityInterceptor securityInterceptor = http .getSharedObject(FilterSecurityInterceptor.class); web.securityInterceptor(securityInterceptor); }); } ``` 這裡有一個關鍵的方法`getHttp()` ```java protected final HttpSecurity getHttp() throws Exception { if (http != null) { return http; } DefaultAuthenticationEventPublisher eventPublisher = objectPostProcessor .postProcess(new DefaultAuthenticationEventPublisher()); localConfigureAuthenticationBldr.authenticationEventPublisher(eventPublisher); AuthenticationManager authenticationManager = authenticationManager(); authenticationBuilder.parentAuthenticationManager(authenticationManager); authenticationBuilder.authenticationEventPublisher(eventPublisher); // 獲取建立共享的物件 Map, Object> sharedObjects = createSharedObjects(); http = new HttpSecurity(objectPostProcessor, authenticationBuilder, sharedObjects); if (!disableDefaults) { // @formatter:off http .csrf().and() .addFilter(new WebAsyncManagerIntegrationFilter()) .exceptionHandling().and() .headers().and() .sessionManagement().and() .securityContext().and() .requestCache().and() .anonymous().and() .servletApi().and() .apply(new DefaultLoginPageConfigurer<>()).and() .logout(); // @formatter:on ClassLoader classLoader = this.context.getClassLoader(); List defaultHttpConfigurers = SpringFactoriesLoader.loadFactories(AbstractHttpConfigurer.class, classLoader); for (AbstractHttpConfigurer configurer : defaultHttpConfigurers) { http.apply(configurer); } } // httpHttpSecurity 的表單配置 configure(http); return http; } ``` 我們簡單列舉幾個重要的方法 ```java // 根據系統載入的AuthenticationManagerBuilder 在裝配使用者 protected UserDetailsService userDetailsService() { AuthenticationManagerBuilder globalAuthBuilder = context .getBean(AuthenticationManagerBuilder.class); return new UserDetailsServiceDelegator(Arrays.asList( localConfigureAuthenticationBldr, globalAuthBuilder)); } ``` ```java protected void configure(HttpSecurity http) throws Exception { logger.debug("Using default configure(HttpSecurity). If subclassed this will potentially override subclass configure(HttpSecurity)."); // 資源保護 http .authorizeRequests() .anyRequest().authenticated() .and() // 認證頁面 .formLogin().and() // HTTP Basic authentication. .httpBasic(); } ``` 上面我們都是通過啟動日誌的資訊來理解應用在啟動時到底做了什麼,載入了什麼關鍵資訊,接下來我們將通過執行時的日誌看來看一下我們在認證過程中是如何進行使用者名稱密碼的校驗的。 ### 登入流程 我們開啟瀏覽器輸入`localhost:8080` 由於我們沒有進行登入,所以會被`redirecting`到登入頁面。我們一起過濾一下控制檯資訊,抓取到關鍵的資訊。 ![](https://i.loli.net/2020/04/12/zAcdNX2fU7OhDP3.gif) 我們看到,這裡載入了各種過濾器,當訪問`/`時沒發現並沒有登入,則重定向到預設的`/login`頁面,這也是`spirng security`的核心。今天重點討論登入流程,我們先清空控制檯,用正確的使用者名稱和密碼登入進去。 ![](https://i.loli.net/2020/04/12/tEOGu6LHUPj9msg.jpg) 從控制檯我們可以看到很多的過濾器,我們至關注認證流程的一部分,已上圖為準。 #### 1、UsernamePasswordAuthenticationFilter 這理解這個過濾器前,我們先從他的父類`AbstractAuthenticationProcessingFilter` 入手,既然是過濾器,我們既要入`doFilter`入手, 這裡是關鍵的流程,子類只是做具體的實現,我們稍後再看 ```java public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { // 請求的轉化 HttpServletRequest request = (HttpServletRequest)req; HttpServletResponse response = (HttpServletResponse)res; if (!this.requiresAuthentication(request, response)) { chain.doFilter(request, response); } else { if (this.logger.isDebugEnabled()) { this.logger.debug("Request is to process authentication"); } Authentication authResult; try { // 關鍵的認證方法,交由子類來實現,我們到子類看 authResult = this.attemptAuthentication(request, response); if (authResult == null) { return; } this.sessionStrategy.onAuthentication(authResult, request, response); } catch (InternalAuthenticationServiceException var8) { this.logger.error("An internal error occurred while trying to authenticate the user.", var8); this.unsuccessfulAuthentication(request, response, var8); return; } catch (AuthenticationException var9) { this.unsuccessfulAuthentication(request, response, var9); return; } if (this.continueChainBeforeSuccessfulAuthentication) { chain.doFilter(request, response); } // 返回認證成功 this.successfulAuthentication(request, response, chain, authResult); } } ``` 上面的關鍵方法`attemptAuthentication(request, response);`在`UsernamePasswordAuthenticationFilter`中 ```java 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()); } else { // 通過“username”拿到使用者名稱 String username = this.obtainUsername(request); // 通過"password" 拿到密碼 String password = this.obtainPassword(request); if (username == null) { username = ""; } if (password == null) { password = ""; } username = username.trim(); // 傳入UsernamePasswordAuthenticationToken構造方法,此類是Authentication的子類 // 此時還沒有認證(false) UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password); this.setDetails(request, authRequest); // 交由AuthenticationManager 去處理 return this.getAuthenticationManager().authenticate(authRequest); } } ``` 在`UsernamePasswordAuthenticationFilter` 的關鍵流程中,我們將請求的引數進行符合入參的封裝, #### 2、AuthenticationManager `AuthenticationManager `本身不包含認證邏輯,其核心是用來管理所有的 `AuthenticationProvider`,通過交由合適的 `AuthenticationProvider` 來實現認證。 #### 3、AuthenticationProvider `Spring Security` 支援多種認證邏輯,每一種認證邏輯的認證方式其實就是一種 `AuthenticationProvider`。通過 `getProviders() `方法就能獲取所有的` AuthenticationProvider`,通過` provider.supports() `來判斷 provider 是否支援當前的認證邏輯。 當選擇好一個合適的` AuthenticationProvider` 後,通過 `provider.authenticate(authentication)` 來讓 `AuthenticationProvider `進行認證。 ```java public Authentication authenticate(Authentication authentication) throws AuthenticationException { Class toTest = authentication.getClass(); AuthenticationException lastException = null; AuthenticationException parentException = null; Authentication result = null; Authentication parentResult = null; boolean debug = logger.isDebugEnabled(); for (AuthenticationProvider provider : getProviders()) { // 判斷是否是其支援的provider if (!provider.supports(toTest)) { continue; } if (debug) { logger.debug("Authentication attempt using " + provider.getClass().getName()); } try { // 由具體的provider去進行處理 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; } ``` #### 4、AbstractUserDetailsAuthenticationProvider 表單登入的 `AuthenticationProvider `主要是由 `AbstractUserDetailsAuthenticationProvider` 來進行處理的,我們來看下它的 `authenticate()`方法。 ```java 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; // 預設從快取中去,如果沒有則呼叫retrieveUser UserDetails user = this.userCache.getUserFromCache(username); if (user == null) { cacheWasUsed = false; try { 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); 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(); } // 認證成功後放入認證成功的資訊,裡面也是放入傳入UsernamePasswordAuthenticationToken另一個構造方法 return createSuccessAuthentication(principalToReturn, authentication, user); } ``` 那麼關鍵的`retrieveUser`裡面是什麼樣呢? ```java protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException { prepareTimingAttackProtection(); try { // 用具體的UserDetailSercvice去獲取使用者資訊 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); } } ``` 由於我們的使用者資訊是在`UserDetailsServiceAutoConfiguration` 的配置類中生成了 `InMemoryUserDetailsManager`,所以這裡的`loadUserByUsername`的程式碼則是這樣 ```java public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { UserDetails user = users.get(username.toLowerCase()); if (user == null) { throw new UsernameNotFoundException(username); } return new User(user.getUsername(), user.getPassword(), user.isEnabled(), user.isAccountNonExpired(), user.isCredentialsNonExpired(), user.isAccountNonLocked(), user.getAuthorities()); } ``` 在記憶體中維護的使用者中去獲取,那麼如果是其他的使用者儲存則需要對應的獲取方式,如果是儲存在資料庫那麼就需要通過`sq`l語句去獲取了,感興趣的可以直接點選`JdbcUserDetailsManager`檢視相關程式碼。 其實真個認證的流程到這裡也就結束了,至於成功或失敗後的邏輯最後還是回到了`UsernamePasswordAuthenticationFilter`中的結果,如果是成功`this.successfulAuthentication(request, response, chain, authResult);` ```java protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException { if (this.logger.isDebugEnabled()) { this.logger.debug("Authentication success. Updating SecurityContextHolder to contain: " + authResult); } // 將認證結果放入到上下文中 SecurityContextHolder.getContext().setAuthentication(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); } ``` ### 總結 以上便是`spring security`的認證流程,沒想到篇幅會這麼長,斷點追蹤的方式很痛苦,大致方向應該是對的,基本的認證流程也應該浮出水面了。本篇主要是從自動配置的方式出發,後續將展示其他的配置方式甚至自定義認證流程,加油!!!