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

黑米麵包派發表於2020-04-27

上面我們一起開始了Spring Security的初體驗,並通過簡單的配置甚至零配置就可以完成一個簡單的認證流程。可能我們都有很大的疑惑,這中間到底發生了什麼,為什麼簡單的配置就可以完成一個認證流程啊,可我啥都沒看見,沒有寫頁面,沒有寫介面。這一篇我們將深入到原始碼層面一起來了解一下spring security到底是怎麼工作的。

準備工作

在開始原始碼理解前,我們先來做一項基本的準備工作,從日誌中去發現線索,因為我們發現什麼都沒有配置的情況下,他也可以正常的工作,並給我們預置了一個臨時的使用者user。那麼他肯定是在工程啟動的時候做了什麼事情,上一篇我們也提到了是如果生成user使用者和密碼的。這篇我們將仔細的去了解一下。

1、首先我們配置在applicaiton.yml中調整一下日誌級別

logging:
  level:
    org.springframework.security: debug

我們將security相關的日誌列印出來,一起來啟動或者執行的時候到底發生了什麼。

2、啟動spring-security-basic 工程

!!!找到了

日誌過濾

(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 不管!找到原始碼再說

@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
@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

@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<String> 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

原來,首先他是個配置註解,也importWebSecurityConfiguration

@Configuration(proxyBeanMethods = false)
public class WebSecurityConfiguration implements ImportAware, BeanClassLoaderAware {
	// 1、宣告一個 webSecurity 一起來看一下他是什麼時候初始化的
	private WebSecurity webSecurity;
	// 2、是否為除錯模式
	private Boolean debugEnabled;
	private List<SecurityConfigurer<Filter, WebSecurity>> webSecurityConfigurers;

	private ClassLoader beanClassLoader;
	// 3、關鍵點,後置物件處理器,用來初始化物件
	@Autowired(required = false)
	private ObjectPostProcessor<Object> 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<Object> objectPostProcessor,
    	// 獲取 0 步中獲取到的物件資訊
			@Value("#{@autowiredWebSecurityConfigurersIgnoreParents.getWebSecurityConfigurers()}") List<SecurityConfigurer<Filter, WebSecurity>> 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<Filter, WebSecurity> 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<Filter, WebSecurity> 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

public abstract class AbstractSecurityBuilder<O> implements SecurityBuilder<O> {
	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;
}
@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(),其實就是開始WebSecurityConfigurerAdapterinit()方法。說了這裡可能有的同學就會比較熟悉了,這就是關鍵配置的介面卡類。

程式碼稍後貼出來,暫時先不看,到這裡為止,我們才梳理了springboot自動配置中的SecurityAutoConfiguration

下面我們才開始第二個類

2、 UserDetailsServiceAutoConfiguration

@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> passwordEncoder) {
        User user = properties.getUser();
        List<String> 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()方法,其他的都是一些預設的配置

	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()

	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<Class<?>, 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<AbstractHttpConfigurer> defaultHttpConfigurers =
					SpringFactoriesLoader.loadFactories(AbstractHttpConfigurer.class, classLoader);

			for (AbstractHttpConfigurer configurer : defaultHttpConfigurers) {
				http.apply(configurer);
			}
		}
    // httpHttpSecurity 的表單配置
		configure(http);
		return http;
	}

我們簡單列舉幾個重要的方法

// 根據系統載入的AuthenticationManagerBuilder 在裝配使用者
protected UserDetailsService userDetailsService() {
		AuthenticationManagerBuilder globalAuthBuilder = context
				.getBean(AuthenticationManagerBuilder.class);
		return new UserDetailsServiceDelegator(Arrays.asList(
				localConfigureAuthenticationBldr, globalAuthBuilder));
	}
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到登入頁面。我們一起過濾一下控制檯資訊,抓取到關鍵的資訊。

我們看到,這裡載入了各種過濾器,當訪問/時沒發現並沒有登入,則重定向到預設的/login頁面,這也是spirng security的核心。今天重點討論登入流程,我們先清空控制檯,用正確的使用者名稱和密碼登入進去。

從控制檯我們可以看到很多的過濾器,我們至關注認證流程的一部分,已上圖為準。

1、UsernamePasswordAuthenticationFilter

這理解這個過濾器前,我們先從他的父類AbstractAuthenticationProcessingFilter 入手,既然是過濾器,我們既要入doFilter入手, 這裡是關鍵的流程,子類只是做具體的實現,我們稍後再看

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

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 進行認證。

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();

		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()方法。

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裡面是什麼樣呢?

	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的程式碼則是這樣

	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());
	}

在記憶體中維護的使用者中去獲取,那麼如果是其他的使用者儲存則需要對應的獲取方式,如果是儲存在資料庫那麼就需要通過sql語句去獲取了,感興趣的可以直接點選JdbcUserDetailsManager檢視相關程式碼。

其實真個認證的流程到這裡也就結束了,至於成功或失敗後的邏輯最後還是回到了UsernamePasswordAuthenticationFilter中的結果,如果是成功this.successfulAuthentication(request, response, chain, authResult);

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的認證流程,沒想到篇幅會這麼長,斷點追蹤的方式很痛苦,大致方向應該是對的,基本的認證流程也應該浮出水面了。本篇主要是從自動配置的方式出發,後續將展示其他的配置方式甚至自定義認證流程,加油!!!

(完)

相關文章