SpringSecurity认证过程详解

198 阅读8分钟

1. Spring Security流程介绍

1.1 过滤器链

我们知道请求进入服务器端后会经过一连串的过滤器,而Spring Security本质上也是一连串的过滤器,这一串过滤器会以一个独立的Filter插入到FilterChain中,就是FilterChainProxy。

其简化流程图为:

未命名文件 (1).png

1.2 Spring Security Filter

Spring Security有很多很多的过滤器,处理过程也相当复杂,但我们只需要专注于他最核心的两个功能,认证(Authentication)和授权(Authorization),忽略其他过滤器,可大大简化其流程,便于我们学习和理解。

认证和授权是由两个核心的过滤器实现的:

  • 认证处理过滤器:AbstractAuthenticationProcessiongFilter
    • 认证管理器:AuthenticationManager
  • 请求鉴权过滤器:AbstractSecurityInterceptor
    • 决策管理器:AccessDecisionManager

FilterChainProxy是一个代理,真正起作用的是一个个Filter,这些Filter作为Bean被spring管理,是SpringSecurity的核心。每个Filter都有各自的职责,不直接处理认证和授权,而是交由认证管理器和决策管理器实现。

其简化流程图为:

未命名文件 (2).png

2 Spring Security认证

2.1 认证流程图

未命名文件 (3).png

2.2 从源码分析认证过程

用户请求携带用户名与密码进入UsernamePasswordAuthenticationFilter UsernamePasswordAuthenticationFilter继承AbstractAuthenticationProcessingFilter,会先执行AbstractAuthenticationProcessingFilter中的doFilter()方法,doFilter()方法中attemptAuthentication()为认证方法,该方法是抽象方法,在UsernamePasswordAuthenticationFilter中重写

2.2.1 AbstractAuthenticationProcessingFilter

进入过滤器后先执行AbstractAuthenticationProcessingFilter.doFilter()方法

1. doFilter()

流程为:

  1. 调用attemptAuthentication()方法进行认证,该方法为抽象方法,子类为UsernamePasswordAuthenticationFilter
  2. 认证失败,执行unsuccessfulAuthentication()方法进入认证失败处理器执行认证失败逻辑
  3. 认证成功,执行successfulAuthentication()方法进入认证成功处理器执行认证成功逻辑
private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
			throws IOException, ServletException {
		if (!requiresAuthentication(request, response)) {
			chain.doFilter(request, response);
			return;
		}
		try {
                //尝试进行认证,该方法为抽象方法,具体实现在子类UsernamePasswordAuthenticationFilter中
			Authentication authenticationResult = attemptAuthentication(request, response);
			if (authenticationResult == null) {
				// return immediately as subclass has indicated that it hasn't completed
				return;
			}
			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);
		}
	}

}

2.2.2 UsernamePasswordAuthenticationFilter

进入UsernamePasswordAuthenticationFilter的调用attemptAuthentication()方法

1. attemptAuthentication()

流程为:

  1. 获取提交的用户名和密码
  2. 获取AuthenticationManager对象,调用AuthenticationManager的authenticate()方法,参数为未经认证的Authentication,返回一个经过认证的Authentication
@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.trim() : "";
		String password = obtainPassword(request);
		password = (password != null) ? password : "";
                //根据传入的用户名和密码生成一个未经认证的Token,是Authentication的子类
		UsernamePasswordAuthenticationToken authRequest = UsernamePasswordAuthenticationToken.unauthenticated(username,
				password);
		// Allow subclasses to set the "details" property
		setDetails(request, authRequest);
                //调用AuthenticationManager中的authenticate()方法,参数为未经认证的Authentication,返回返回一个Authentication对象
		return this.getAuthenticationManager().authenticate(authRequest);
	}

2.2.3 AuthenticationManager

AuthenticationManager是接口,仅有一个方法authenticate(),其实现类为ProviderManager。

调用进入ProviderManager的Authenticate()方法中 该类中有一系列的AuthenticationProvider,循环判断每个AuthenticationProvier是否支持当前请求 ,支持则调用该AuthentticationProvider中的authenticate()方法,不支持则继续判断下一个AuthenticationProvider 若所有的AuthenticationProvider都不支持,则最后调用全局AuthenticationProvider进行一次认证

1. authenticate()

流程为:

  1. 循环遍历每个provider,调用其supports()方法,判断该provider是否支持当前请求
  2. 支持则调用provider中的authenticate()方法进行认证,不支持则遍历判断下一个provider
  3. 若所有provider都不支持,则调用父Provider(也称为全局Provider)进行最后一次认证
@Override
	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;
		int currentPosition = 0;
		int size = this.providers.size();
                //循环每个provider
		for (AuthenticationProvider provider : getProviders()) {
                //判断该provider是否支持当前请求,不支持则继续判断下一个provider
			if (!provider.supports(toTest)) {
				continue;
			}
			if (logger.isTraceEnabled()) {
				logger.trace(LogMessage.format("Authenticating request with %s (%d/%d)",
						provider.getClass().getSimpleName(), ++currentPosition, size));
			}
			try {
                        //支持则调用provider中的authenticate()方法
				result = provider.authenticate(authentication);
				if (result != null) {
					copyDetails(authentication, result);
					break;
				}
			}
			catch (AccountStatusException | InternalAuthenticationServiceException ex) {
				prepareException(ex, authentication);
				// SEC-546: Avoid polling additional providers if auth failure is due to
				// invalid account status
				throw ex;
			}
			catch (AuthenticationException ex) {
				lastException = ex;
			}
		}
		if (result == null && this.parent != null) {
			// Allow the parent to try.
			try {
                        //若所有的provider都不支持当前请求,则最后调用全局provider进行认证一次
				parentResult = this.parent.authenticate(authentication);
				result = parentResult;
			}
			catch (ProviderNotFoundException ex) {
				// 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 ex) {
				parentException = ex;
				lastException = ex;
			}
		}
		if (result != null) {
			if (this.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 then it
			// will publish an AuthenticationSuccessEvent
			// This check prevents a duplicate AuthenticationSuccessEvent if the parent
			// AuthenticationManager already published it
			if (parentResult == null) {
				this.eventPublisher.publishAuthenticationSuccess(result);
			}

			return result;
		}

		// Parent was null, or didn't authenticate (or throw an exception).
		if (lastException == null) {
			lastException = new ProviderNotFoundException(this.messages.getMessage("ProviderManager.providerNotFound",
					new Object[] { toTest.getName() }, "No AuthenticationProvider found for {0}"));
		}
		// If the parent AuthenticationManager was attempted and failed then 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;
	}

2.2.4 AuthenticationProvider

AuthenticationProvider同样为接口,提供supports()和authenticate()两个方法,其实现类AbstractUserDetailsAuthenticationProvider为抽象类,其中有两个方法retrieveUser()和additionalAuthenticationChecks()在其子类中实现,子类为DaoUserDetailsAuthenticationProvider

1. AbstractUserDetailsAuthenticationProvider.authenticate()

流程为:

  1. 从缓存中获取UserDetails对象
  2. 若缓存中不存在,则调用retrieveUser()从数据库中获取,该方法为抽象方法,具体实现在子类DaoUserDetailsAuthenticationProvider中
  3. 调用preAuthenticationChecks.check()方法做认证前的检查,主要是账号的可用性,是否锁定等
  4. 调用additionalAuthenticationChecks()方法进行用户名与密码的验证,该方法为抽象方法,具体实现在子类DaoUserDetailsAuthenticationProvider中
  5. 是否使用了缓存,使用了则将用户信息存入缓存中
  6. 认证成功后生成经过认证的Authhentication对象并返回
@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 {
                        //缓存中无用户则执行retrieveUser()方法从数据库中获取用户
				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;
			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);
	}

2.2.5 DaoUserDetailsAuthenticationProvider

1. DaoUserDetailsAuthenticationProvider.retrieveUser()

流程为:

  1. 获取UserDetailsService对象,调用loadUserByUsername()方法获取用户信息,返回一个UserDetails用户对象

UserDetailsService是接口,当需要从数据库中获取用户信息时,需要程序员实现UserDetailsService接口,重写loadUserByUsername()方法,完成从数据库中获取用户数据的逻辑,返回对象为UserDetails,重写loadUserByUsername()方法时,可以自定义一个用户类,封装用户信息,继承UserDetails类来返回。

@Override
	protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication)
			throws AuthenticationException {
		prepareTimingAttackProtection();
		try {
                //调用UserDetailsService中的loadUserByUsername()方法获取用户信息
			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);
		}
	}
2. DaoUserDetailsAuthenticationProvider.additionalAuthenticationChecks()

流程为:

  1. 将提交的明文进行加密,并和从数据库中获取的密文进行比对
  @Override
	protected void additionalAuthenticationChecks(UserDetails userDetails,
			UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
		if (authentication.getCredentials() == null) {
			this.logger.debug("Failed to authenticate since no credentials provided");
			throw new BadCredentialsException(this.messages
					.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
		}
		String presentedPassword = authentication.getCredentials().toString();
                //将提交的明文进行加密,并和从数据库中获取的密文进行比对
		if (!this.passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
			this.logger.debug("Failed to authenticate since password does not match stored value");
			throw new BadCredentialsException(this.messages
					.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
		}
	}

至此,认证流程完成。

3 认证流程总结

由上面源码分析可以看出一共使用了如下几个类,可将其分为四个部分去梳理他们的功能:

未命名文件 (5).png

后面按照这四个部分再梳理一次,所使用的类如下:

  • UsernamePasswordAuthenticationFilter

  • AbstractAuthenticationProcessingFilter

  • ProviderManager

  • AuthenticationManager

  • AuthenticationProvider

  • AbstractUserDetailsAuthenticationProvider

  • DaoAuthenticationProvider

  • UserDetailsService

  • UserDetails

3.1 过滤器

这一组的职责是将用户提交的用户名与密码数据封装成未经认证的Authentication对象然后交由ProviderManager进行认证。

  • AbstractAuthenticationProcessingFilter
  • UsernamePasswordAuthenticationFilter

AbstractAuthenticationProcessingFilter为认证过滤器的入口,执行他的doFilter()方法,开始进行表单用户名与密码的验证,在子类AbstractAuthenticationProcessingFilter中将用户名与密码封装成一个未经认证的Authentication对象,然后交由ProviderManger(认证管理器)进行认证。

3.2 认证管理器

这一组的职责是选择一个合适的AuthenticationProvider进行认证

  • AuthenticationManager
  • ProviderManager

未经认证的Authentication进入ProviderManager之后,会有一系列的AuthenticationProvider,依次进行判断选择,选择一个支持当前请求的AuthenticationProvider进行认证

3.3 AuthenticationProvider

这一组的职责是作为一组固定的模板,提供了完整的验证流程,暴露UserDetailsService接口交由用户自行实现。

  • AuthenticationProvider
  • AbstractUserDetailsAuthenticationProvider
  • DaoAuthenticationProvider

AuthenticationProvider是一组接口,AbstractUserDetailsAuthenticationProvider是实现这组接口的抽象类,在抽象类的authenticate()方法中定义了验证流程,而其中核心的两个方法作为抽象方法,让其子类DaoAuthenticationProvider进行完善,在DaoAuthenticationProvider中使用了UserDetailsService中的方法来获取用户信息,该接口需程序员来实现,自定义用户数据和用户数据的获取逻辑。

3.4 自定义实现

这一组交由程序员进行处理,用户自定义用户数据和用户数据的获取逻辑。

  • UserDetailsService
  • UserDetails

实现UserDetailsService接口,重写loadUserByUsername()方法,自定义用户数据获取逻辑,自定义用户数据返回类,封装用户数据并继承UserDetails。