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


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

(完)


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM