当前位置: 首页 > article >正文

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 方法这个操作的实现方式有点特殊,就像下面这样:


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 方法;通过手动维护过滤器迭代的索引,保证了在回调时准确的获取下一个迭代器。

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


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 接口的实现,定义了身份认证的核心处理逻辑。

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 进行处理。

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

	public boolean supports(Class<?> authentication) {
		return (UsernamePasswordAuthenticationToken.class
				.isAssignableFrom(authentication));
	}

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 来实现。


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。

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

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

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 封装的用户密码与检索出来的用户对象通过加密工具进行匹配来完成认证逻辑。

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 对象。


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 对象,从而获取用户身份、请求凭据、与用户权限。


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 提供了一系列的端点实现,根据项目需要你可以进行配置或者自定义任何你想要的认证端点处理逻辑。
	private void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response,
			AuthenticationException failed) throws IOException, ServletException {
		SecurityContextHolder.clearContext();
		this.failureHandler.onAuthenticationFailure(request, response, failed);
	}

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 中方便后续取用。

http://www.kler.cn/news/361272.html

相关文章:

  • linux链接、目标文件全解析
  • TWS充电盒:【电源管理芯片汇总】
  • 3184. 构成整天的下标对数目 I
  • Telegram mini app开发极简示例
  • 批量合并PDF 文件的 5 大解决方案
  • springboot+vue美食推荐商城的设计与实现+万字lw
  • 格姗知识圈博客网站开源了!
  • 苍穹外卖学习笔记(三十二最终篇)
  • 构建高效智慧社区:Spring Boot Web框架应用
  • Ubuntu配置FTP
  • 基于图像拼接开题报告
  • Python 正则
  • Prompt提示词设计:如何让你的AI对话更智能?
  • EasyExcel自定义下拉注解的三种实现方式
  • 容灾与云计算概念
  • 加密DNS有什么用?
  • 网络安全——防火墙技术
  • 在 Kylin Linux 上安装 PostgreSQL 以下是安装 PostgreSQL 的步骤:
  • linux命令基础
  • 边缘计算网关兼容多种通信协议实现不同设备和系统互联互通