Spring security 如何进行身份认证

阅读本文之前,请投票支持这款 全新设计的脚手架 ,让 Java 再次伟大!

Filter

Spring security 的运行依赖于一系列 filter chains ,其中每一组 filter chain 对应了一种类型的 request type。

当引入 spring security 框架时,会将 security filter chains 注册到 servlet filter chain 上,维护这些 security filter chains 的类就是 FilterChainProxy。

VirtualFilterChain

上图中展示了默认情况下 security 加载的 11 个过滤器。这些过滤器组成的 chains 被命名为 VirtualFilterChain,用以和原生的 servlet chain 做区分。

VirtualFilterChain 有一个 List additionalFilters 字段持有 security 加载的所有过滤器。只要通过遍历 additionalFilters 的每个元素并调用每个元素上的 doFilter 方法,就可以实现请求在链条上传播的功能。

不过,调用每个元素的 doFilter 方法这个操作的实现方式有点特殊,就像下面这样:

java 复制代码
currentPosition++;

Filter nextFilter = additionalFilters.g(currentPosition - 1);

if (logger.isDebugEnabled()) {
    logger.debug(UrlUtils.buildRequestUrl(firewalledRequest)
            + " at position " + currentPosition + " of " + size
            + " in additional filter chain; firing Filter: '"
            + nextFilter.getClass().getSimpleName() + "'");
}

nextFilter.doFilter(request, response, this);
  1. 在调用过滤器的 doFilter 方法时, this 对象会被传递到被调用过滤器中。
  2. 在调用对象的方法中手动维护 过滤器迭代的索引。

这是有点取巧的设计:采用将 this 传递到被调用过滤器中的方式,是为了在调用对象中使用回调------这里的回调就是指调用下一个过滤器的 doFilter 方法;通过手动维护过滤器迭代的索引,保证了在回调时准确的获取下一个迭代器。

这样的做法在避免了维护一套复杂数据结构的前提下,使这条过链上的所有迭代器都拥有了下一个迭代器元素的指针,从而间接实现了「责任链设计模式」。

java 复制代码
private static class VirtualFilterChain implements FilterChain {
		private final FilterChain originalChain;
		private final List<Filter> additionalFilters;
		private final FirewalledRequest firewalledRequest;
		private final int size;
		private int currentPosition = 0;

		private VirtualFilterChain(FirewalledRequest firewalledRequest,
				FilterChain chain, List<Filter> additionalFilters) {
			this.originalChain = chain;
			this.additionalFilters = additionalFilters;
			this.size = additionalFilters.size();
			this.firewalledRequest = firewalledRequest;
		}

		@Override
		public void doFilter(ServletRequest request, ServletResponse response)
				throws IOException, ServletException {
			if (currentPosition == size) {
				if (logger.isDebugEnabled()) {
					logger.debug(UrlUtils.buildRequestUrl(firewalledRequest)
							+ " reached end of additional filter chain; proceeding with original chain");
				}

				// Deactivate path stripping as we exit the security filter chain
				this.firewalledRequest.reset();

				originalChain.doFilter(request, response);
			}
			else {
				currentPosition++;

				Filter nextFilter = additionalFilters.get(currentPosition - 1);

				if (logger.isDebugEnabled()) {
					logger.debug(UrlUtils.buildRequestUrl(firewalledRequest)
							+ " at position " + currentPosition + " of " + size
							+ " in additional filter chain; firing Filter: '"
							+ nextFilter.getClass().getSimpleName() + "'");
				}

				nextFilter.doFilter(request, response, this);
			}
		}
	}

AbstractAuthenticationProcessingFilter. doFilter()

security 加载的每种过滤器都有自己的使命和职责,其中负责表单登陆的是 UsernamePasswordAuthenticationFilter。

security 中大部分和身份认证相关的过滤器都继承自 AbstractAuthenticationProcessingFilter,UsernamePasswordAuthenticationFilter 也不例外。父类 AbstractAuthenticationProcessingFilter 是一个 Servlet Filter 接口的实现,定义了身份认证的核心处理逻辑。

java 复制代码
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
			throws IOException, ServletException {

		HttpServletRequest request = (HttpServletRequest) req;
		HttpServletResponse response = (HttpServletResponse) res;

		// 当前请求还未认证
		if (!requiresAuthentication(request, response)) {
			chain.doFilter(request, response);

			return;
		}

		if (logger.isDebugEnabled()) {
			logger.debug("Request is to process authentication");
		}

		Authentication authResult;

		try {
         // 执行认证功能
			authResult = attemptAuthentication(request, response);
			if (authResult == null) {
				// return immediately as subclass has indicated that it hasn't completed
				// authentication
				return;
			}
			sessionStrategy.onAuthentication(authResult, request, response);
		}
		catch (InternalAuthenticationServiceException failed) {
			logger.error(
					"An internal error occurred while trying to authenticate the user.",
					failed);
			unsuccessfulAuthentication(request, response, failed);

			return;
		}
		catch (AuthenticationException failed) {
			// Authentication failed
			unsuccessfulAuthentication(request, response, failed);

			return;
		}

		// Authentication success
		if (continueChainBeforeSuccessfulAuthentication) {
			chain.doFilter(request, response);
		}

		successfulAuthentication(request, response, chain, authResult);
	}

如果当前请求还未认证,父类 AbstractAuthenticationProcessingFilter 会调用由子类 UsernamePasswordAuthenticationFilter 实现的 attemptAuthentication 进行身份认证。

UsernamePasswordAuthenticationFilter. attemptAuthentication()

在父类方法中定义共通处理内容,但将自定义部分留给子类来完成的设计方式,是模板方法设计模式的体现。

attemptAuthentication 方法将用户名与密码封装为一个 UsernamePasswordAuthenticationToken 对象,再将此对象交由 AuthenticationManager 接口的实现类 ProviderManager 进行处理。

java 复制代码
public Authentication attemptAuthentication(HttpServletRequest request,
			HttpServletResponse response) throws AuthenticationException {
		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();

		UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
				username, password);

		// Allow subclasses to set the "details" property
		setDetails(request, authRequest);

      // 将用户名与密码交由 AuthenticationManager() 进行认证。
		return this.getAuthenticationManager().authenticate(authRequest);
	}

ProviderManager.authenticate()

ProviderManager 自己并不会执行身份认证。它会将请求委托给被他管理的一系列 AuthenticationProvider 集合。AuthenticationProvider 集合中包含一个名为 DaoAuthenticationProvider 的 Provider ,专门负责处理基于 UsernamePasswordAuthenticationToken 的认证请求。

DaoAuthenticationProvider.supports()

java 复制代码
	public boolean supports(Class<?> authentication) {
		return (UsernamePasswordAuthenticationToken.class
				.isAssignableFrom(authentication));
	}
java 复制代码
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()) {
			if (!provider.supports(toTest)) {
				continue;
			}

			if (debug) {
				logger.debug("Authentication attempt using "
						+ provider.getClass().getName());
			}

			try {
				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;
	}

AbstractUserDetailsAuthenticationProvider.authenticate()

DaoAuthenticationProvider 的设计也运用了模板方法设计模式------authenticate 模板方法定义在了父类 AbstractUserDetailsAuthenticationProvider ,而具体的认证逻辑细节都通过子类 DaoAuthenticationProvider 来实现。

java 复制代码
public Authentication authenticate(Authentication authentication)
			throws AuthenticationException {
          // 1 - A 检索用户
			user = retrieveUser(username,(UsernamePasswordAuthenticationToken) authentication);
			// 2 - B 检查用户状态是否有效
			preAuthenticationChecks.check(user);
			// 3 - C 用户认证
			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();
		}

		return createSuccessAuthentication(principalToReturn, authentication, user);
	}

DaoAuthenticationProvider. retrieveUser()

检索用户功能是通过返回一个 UserDetails 对象 loadUserByUsername 方法来完成的。UserDetails 是一个接口,其中规范了使用 spring security 时用户对象应该遵守的行为和至少应该具备的字段。这是通过 UserDetails 的一系列行为的返回值来决定的。

这也意味着你可以自定义这些行为来决定一个用户是否有效------这是面向接口而不是面向实现的经典案例,言下之意你的用户实体需要实现 UserDetails 与 loadUserByUsername。

java 复制代码
// A
protected final UserDetails retrieveUser(String username,
			UsernamePasswordAuthenticationToken authentication)
			throws AuthenticationException {
		prepareTimingAttackProtection();
		try {
			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);
		}
	}

AccountStatusUserDetailsChecker. check()

由于定义了用户对象的行为和字段标准,检查用户是否有效的功能自然也就可以提供了。

java 复制代码
public class AccountStatusUserDetailsChecker implements UserDetailsChecker, MessageSourceAware {

	protected MessageSourceAccessor messages = SpringSecurityMessageSource
			.getAccessor();
	// B
	public void check(UserDetails user) {
		if (!user.isAccountNonLocked()) {
			throw new LockedException(messages.getMessage(
					"AccountStatusUserDetailsChecker.locked", "User account is locked"));
		}

		if (!user.isEnabled()) {
			throw new DisabledException(messages.getMessage(
					"AccountStatusUserDetailsChecker.disabled", "User is disabled"));
		}

		if (!user.isAccountNonExpired()) {
			throw new AccountExpiredException(
					messages.getMessage("AccountStatusUserDetailsChecker.expired",
							"User account has expired"));
		}

		if (!user.isCredentialsNonExpired()) {
			throw new CredentialsExpiredException(messages.getMessage(
					"AccountStatusUserDetailsChecker.credentialsExpired",
					"User credentials have expired"));
		}
	}

DaoAuthenticationProvider.additionalAuthenticationChecks()

additionalAuthenticationChecks 方法通过将 UsernamePasswordAuthenticationToken 封装的用户密码与检索出来的用户对象通过加密工具进行匹配来完成认证逻辑。

java 复制代码
protected void additionalAuthenticationChecks(UserDetails userDetails,
			UsernamePasswordAuthenticationToken authentication)
			throws AuthenticationException {
		if (authentication.getCredentials() == null) {
			logger.debug("Authentication failed: no credentials provided");

			throw new BadCredentialsException(messages.getMessage(
					"AbstractUserDetailsAuthenticationProvider.badCredentials",
					"Bad credentials"));
		}

		String presentedPassword = authentication.getCredentials().toString();

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

AbstractUserDetailsAuthenticationProvider.createSuccessAuthentication()

认证成功后,调用 createSuccessAuthentication 方法,将 principal -> 用户身份,authentication -> 请求凭据,权限 -> user.getAuthorities() 组合成 Authentication 对象。

java 复制代码
protected Authentication createSuccessAuthentication(Object principal,
			Authentication authentication, UserDetails user) {
		// Ensure we return the original credentials the user supplied,
		// so subsequent attempts are successful even with encoded passwords.
		// Also ensure we return the original getDetails(), so that future
		// authentication events after cache expiry contain the details
		UsernamePasswordAuthenticationToken result = new UsernamePasswordAuthenticationToken(
				principal, authentication.getCredentials(),
				authoritiesMapper.mapAuthorities(user.getAuthorities()));
		result.setDetails(authentication.getDetails());

		return result;
	}

AbstractAuthenticationProcessingFilter.successfulAuthentication()

最后再回到 AbstractAuthenticationProcessingFilter 的 successfulAuthentication 方法。

由于将认证结果放入了 SecurityContextHolder.getContext().setAuthentication(authResult) 这个 ThreadLocal 对象,所以在每一个 requestScope 里你都可以通过 SecurityContextHolder.getContext 得到认证成功的 Authentication 对象,从而获取用户身份、请求凭据、与用户权限。

java 复制代码
protected void successfulAuthentication(HttpServletRequest request,
			HttpServletResponse response, FilterChain chain, Authentication authResult)
			throws IOException, ServletException {

		if (logger.isDebugEnabled()) {
			logger.debug("Authentication success. Updating SecurityContextHolder to contain: "
					+ authResult);
		}

		SecurityContextHolder.getContext().setAuthentication(authResult);

		rememberMeServices.loginSuccess(request, response, authResult);

		// Fire event
		if (this.eventPublisher != null) {
			eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(
					authResult, this.getClass()));
		}

		successHandler.onAuthenticationSuccess(request, response, authResult);
	}

AbstractAuthenticationProcessingFilter. unsuccessfulAuthentication()

当用户提交了非法的凭据时,DaoAuthenticationProvider 会抛出一个 BadCredentialsException 异常,这个异常是 AuthenticationException 异常的子类;

异常沿着调用链传递到 AbstractAuthenticationProcessingFilter 后会被捕获,捕获逻辑就定义在 unsuccessfulAuthentication 方法中。

  • 清空当前「用户空间」中的信息。
  • 通过 AuthenticationEntryPointFailureHandler. onAuthenticationFailure 方法将请求和异常指派到身份认证端点:authenticationEntryPoint.commence 方法中进行处理。
  • spring security 提供了一系列的端点实现,根据项目需要你可以进行配置或者自定义任何你想要的认证端点处理逻辑。
java 复制代码
	private void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response,
			AuthenticationException failed) throws IOException, ServletException {
		SecurityContextHolder.clearContext();
		this.failureHandler.onAuthenticationFailure(request, response, failed);
	}
java 复制代码
public class AuthenticationEntryPointFailureHandler implements AuthenticationFailureHandler {

	private final AuthenticationEntryPoint authenticationEntryPoint;

	public AuthenticationEntryPointFailureHandler(AuthenticationEntryPoint authenticationEntryPoint) {
		Assert.notNull(authenticationEntryPoint, "authenticationEntryPoint cannot be null");
		this.authenticationEntryPoint = authenticationEntryPoint;
	}

	@Override
	public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
			AuthenticationException exception) throws IOException, ServletException {
		this.authenticationEntryPoint.commence(request, response, exception);
	}

总结

  1. Spring security 首先由一系列 filter chians 构成。
  2. 每种 security filter 负责不同的职责与功能。其中主要负责用户认证的 filter 为继承了 AbstractAuthenticationProcessingFilter 的 UsernamePasswordAuthenticationFilter。
  3. UsernamePasswordAuthenticationFilter 将用户认证委托给 ProviderManager 处理。
  4. ProviderManager 负责将认证处理委托给一系列 Provider。其中 DaoAuthenticationProvider 专门处理针对 UsernamePasswordAuthenticationToken 的认证处理。
  5. 使用加密工具 PasswordEncoder ,匹配从数据库中查询出指定的用户的密码与用户请求凭据中的密码,完成认证逻辑。
  6. 处理完成后,认证对象会放入 requestScope 中方便后续取用。
相关推荐
小马爱打代码5 分钟前
SpringBoot与Sentinel整合,解决DDoS攻击与异常爬虫请求问题
spring boot·sentinel·ddos
utmhikari1 小时前
【架构艺术】Go语言微服务monorepo的代码架构设计
后端·微服务·架构·golang·monorepo
蜡笔小新星1 小时前
Flask项目框架
开发语言·前端·经验分享·后端·python·学习·flask
计算机学姐1 小时前
基于Asp.net的驾校管理系统
vue.js·后端·mysql·sqlserver·c#·asp.net·.netcore
欢乐少年19043 小时前
SpringBoot集成Sentry日志收集-3 (Spring Boot集成)
spring boot·后端·sentry
夏天的味道٥4 小时前
使用 Java 执行 SQL 语句和存储过程
java·开发语言·sql
冰糖码奇朵6 小时前
大数据表高效导入导出解决方案,mysql数据库LOAD DATA命令和INTO OUTFILE命令详解
java·数据库·sql·mysql
好教员好6 小时前
【Spring】整合【SpringMVC】
java·spring
浪九天7 小时前
Java直通车系列13【Spring MVC】(Spring MVC常用注解)
java·后端·spring
小斌的Debug日记7 小时前
框架基本知识总结 Day16
redis·spring