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

Spring Security 6.1.x 系列(5)—— Servlet 认证体系结构介绍

一、前言

本章主要学习Spring Security中基于Servlet 的认证体系结构,为后续认证执行流程源码分析打好基础。

二、身份认证机制

Spring Security提供个多种认证方式登录系统,包括:

  • Username and Password:使用用户名/密码 方式进行认证登录
  • OAuth 2.0 Login:使用OpenID ConnectOAuth 2.0 方式进行认证登录
  • CAS: 使用CAS企业级单点登录系统 方式进行认证登录
  • SAML 2.0 Login:使用SAML 2.0 方式进行认证登录
  • Remember Me:使用记住我 方式进行认证登录
  • JAAS: 使用JAAS 方式进行认证登录
  • Pre-Authentication Scenarios:使用外部机制 方式进行认证登录
  • X509:使用X509 方式进行认证登录

三、认证组件简介

Spring Security中的认证相关组件:

  • SecurityContextHolder:安全上下文持有者,存储当前认证用户的SecurityContext

  • SecurityContext :安全上下文,包含当期认证用户的Authentication(认证信息),从SecurityContextHolder中获取。

  • Authentication :认证信息,用户提供的用于身份认证的凭据的输入。

  • GrantedAuthority :授予用户的权限(角色、作用域)。

  • AuthenticationManager :认证管理器,是一个接口,定义Spring Security过滤器执行身份认证的API

  • ProviderManager :提供者管理器,是AuthenticationManager的默认实现。

  • AuthenticationProvider : 认证提供者,由ProviderManager选择,用于执行特定类型的身份认证。

  • AuthenticationEntryPoint : 认证入口点,处理认证过程中的认证异常,比如:重定向登录页面

  • AbstractAuthenticationProcessingFilter :抽象认证处理过滤器,一个Filter抽象类,是身份验证的基础。

四、认证组件源码分析

4.1 SecurityContextHolder

SecurityContextHolder(安全上下文持有者)是Spring Security身份认证模型的核心,存储已认证用户详细信息,包含了SecurityContext(安全上下文):

在这里插入图片描述
当用户认证成功后,会将SecurityContext设置到SecurityContextHolder中,后续流程可以用过SecurityContextHolder静态方法直接获取用户信息:

SecurityContext context = SecurityContextHolder.getContext();// 获取 SecurityContext
Authentication authentication = context.getAuthentication();// 获取认证信息
String username = authentication.getName(); // 用户名
Object principal = authentication.getPrincipal(); // 当前用户的信息,通常是UserDetails的实例
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();// 权限

默认策略下,SecurityContextHolder使用ThreadLocal来存储信息,一个已认证的请求线程在过滤器阶段,会获取会话中的认证信息,并保存在当前线程的ThreadLocal中,方便业务代码获取用户信息,线程执行完毕后,FilterChainProxy会自动执行清除。

某些应用程序并不完全适合使用默认策略 ,因为它们使用线程有特定方式。 例如:Swing 客户端可能希望Java中的所有线程都使用相同的SecurityContextHolder,您可以在启动时使用策略进行配置,以指定您希望如何存储SecurityContext

其他应用程序可能希望由安全线程派生的线程也采用相同的安全标识。 您可以通过两种方式更改默认模式:

  • 第一种是设置系统属性,通过System.getProperty("spring.security.strategy")读取设置系统属性。
  • 第二种是在调用静态方法,通过调用SecurityContextHolder.setStrategyName(String strategyName)设置。

SecurityContextHolder提供的strategyName有以下几种:

  • SecurityContextHolder.MODE_GLOBAL
  • SecurityContextHolder.MODE_INHERITABLETHREADLOCAL
  • SecurityContextHolder.MODE_THREADLOCAL

大多数应用程序不需要更改默认值。

4.2 SecurityContex

SecurityContex是一个接口,从SecurityContextHolder中获取,包含了Authentication (认证信息),提供了两个简单方法:

public interface SecurityContext extends Serializable {

	/**
	 * 获取当前已通过身份验证的主体或身份验证请求令牌。
	 */
	Authentication getAuthentication();

	/**
	 * 更改当前已通过身份验证的主体或删除身份验证信息
	 */
	void setAuthentication(Authentication authentication);

}

SecurityContex的实现类SecurityContextImpl也很简单:

public class SecurityContextImpl implements SecurityContext {

	private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;

	private Authentication authentication;

	public SecurityContextImpl() {
	}

	public SecurityContextImpl(Authentication authentication) {
		this.authentication = authentication;
	}

	@Override
	public boolean equals(Object obj) {
		if (obj instanceof SecurityContextImpl) {
			SecurityContextImpl other = (SecurityContextImpl) obj;
			if ((this.getAuthentication() == null) && (other.getAuthentication() == null)) {
				return true;
			}
			if ((this.getAuthentication() != null) && (other.getAuthentication() != null)
					&& this.getAuthentication().equals(other.getAuthentication())) {
				return true;
			}
		}
		return false;
	}

	@Override
	public Authentication getAuthentication() {
		return this.authentication;
	}

	@Override
	public int hashCode() {
		return ObjectUtils.nullSafeHashCode(this.authentication);
	}

	@Override
	public void setAuthentication(Authentication authentication) {
		this.authentication = authentication;
	}

	@Override
	public String toString() {
		StringBuilder sb = new StringBuilder();
		sb.append(getClass().getSimpleName()).append(" [");
		if (this.authentication == null) {
			sb.append("Null authentication");
		}
		else {
			sb.append("Authentication=").append(this.authentication);
		}
		sb.append("]");
		return sb.toString();
	}

}

4.3 Authentication

Authentication是一个接口,在Spring Security中主要有两个作用:

  • 预认证用户信息:此时没有经过认证,作为AuthenticationManager(认证管理器)的一个输入参数,用于提供认证凭证,在此时.isAuthenticated()值为false
  • 当前认证的用户:表示当前经过身份认证的用户,可以从SecurityContext获取当前的Authentication(认证信息)

Authentication包含以下信息:

  • principal:用户主体标识。 使用用户名/密码进行身份验证时,这通常是UserDetails的用户名。

  • credentials:通常是密码。 在许多情况下,在用户通过身份验证后会清除此信息,以确保它不会泄露。

  • authoritiesGrantedAuthority实例是授予用户的高级权限。 下文详细介绍。

Authentication源码如下所示:

public interface Authentication extends Principal, Serializable {
    Collection<? extends GrantedAuthority> getAuthorities();

    Object getCredentials();

    Object getDetails();

    Object getPrincipal();

    boolean isAuthenticated();

    void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException;
}

Authentication接口有很多实现类,对应不同的认证方式。比如:使用用户名/密码进行身份验证时,使用的是UsernamePasswordAuthenticationToken

在这里插入图片描述

4.4 GrantedAuthority

GrantedAuthority实例是授予用户的权限,包括:角色、作用域。可以从Authentication.getAuthorities() 方法获取已认证用户的权限集合。

GrantedAuthority源码如下所示:

public interface GrantedAuthority extends Serializable {
    String getAuthority();
}

只提供一个getAuthority()方法,用于获取已认证用户的权限,默认实现类为SimpleGrantedAuthority,源码如下所示:

public final class SimpleGrantedAuthority implements GrantedAuthority {

	private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;

	private final String role;

	public SimpleGrantedAuthority(String role) {
		Assert.hasText(role, "A granted authority textual representation is required");
		this.role = role;
	}

	@Override
	public String getAuthority() {
		return this.role;
	}

	@Override
	public boolean equals(Object obj) {
		if (this == obj) {
			return true;
		}
		if (obj instanceof SimpleGrantedAuthority sga) {
			return this.role.equals(sga.getAuthority());
		}
		return false;
	}

	@Override
	public int hashCode() {
		return this.role.hashCode();
	}

	@Override
	public String toString() {
		return this.role;
	}

}

4.5 AuthenticationManager

AuthenticationManager(认证管理器)是一个接口,Spring Security过滤器调用其认证方法进行身份认证,默认实现是ProviderManager

AuthenticationManager源码如下所示:

/**
 * Processes an {@link Authentication} request.
 * 认证管理器 实现认证主要是通过AuthenticationManager接口
 * 在实际开发中,我们可能有多种不同的认证方式,例如:用户名+密码、
 * 邮箱+密码、手机号+验证码等,而这些认证方式的入口始终只有一个,那就是AuthenticationManager。
 *
 * @author Ben Alex
 */
public interface AuthenticationManager {

	/**
	 * authenticate()方法主要做三件事:
	 *   如果验证通过,返回Authentication(通常带上authenticated=true)。
	 *   认证失败抛出AuthenticationException
	 *   如果无法确定,则返回null
	 */
	Authentication authenticate(Authentication authentication) throws AuthenticationException;

}

4.6 ProviderManager

ProviderManager(提供者管理器)默认实现了AuthenticationManager 接口。其源码如下所示:

public class ProviderManager implements AuthenticationManager, MessageSourceAware, InitializingBean {

	private static final Log logger = LogFactory.getLog(ProviderManager.class);

	private AuthenticationEventPublisher eventPublisher = new NullEventPublisher();

	private List<AuthenticationProvider> providers = Collections.emptyList();

	protected MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor();

	private AuthenticationManager parent;

	private boolean eraseCredentialsAfterAuthentication = true;

	/**
	 * Construct a {@link ProviderManager} using the given {@link AuthenticationProvider}s
	 * @param providers the {@link AuthenticationProvider}s to use
	 */
	public ProviderManager(AuthenticationProvider... providers) {
		this(Arrays.asList(providers), null);
	}

	/**
	 * Construct a {@link ProviderManager} using the given {@link AuthenticationProvider}s
	 * @param providers the {@link AuthenticationProvider}s to use
	 */
	public ProviderManager(List<AuthenticationProvider> providers) {
		this(providers, null);
	}

	/**
	 * Construct a {@link ProviderManager} using the provided parameters
	 * @param providers the {@link AuthenticationProvider}s to use
	 * @param parent a parent {@link AuthenticationManager} to fall back to
	 */
	public ProviderManager(List<AuthenticationProvider> providers, AuthenticationManager parent) {
		Assert.notNull(providers, "providers list cannot be null");
		this.providers = providers;
		this.parent = parent;
		checkState();
	}

	@Override
	public void afterPropertiesSet() {
		checkState();
	}

	private void checkState() {
		Assert.isTrue(this.parent != null || !this.providers.isEmpty(),
				"A parent AuthenticationManager or a list of AuthenticationProviders is required");
		Assert.isTrue(!CollectionUtils.contains(this.providers.iterator(), null),
				"providers list cannot contain null values");
	}

	/**
	 * Attempts to authenticate the passed {@link Authentication} object.
	 * <p>
	 * The list of {@link AuthenticationProvider}s will be successively tried until an
	 * <code>AuthenticationProvider</code> indicates it is capable of authenticating the
	 * type of <code>Authentication</code> object passed. Authentication will then be
	 * attempted with that <code>AuthenticationProvider</code>.
	 * <p>
	 * If more than one <code>AuthenticationProvider</code> supports the passed
	 * <code>Authentication</code> object, the first one able to successfully authenticate
	 * the <code>Authentication</code> object determines the <code>result</code>,
	 * overriding any possible <code>AuthenticationException</code> thrown by earlier
	 * supporting <code>AuthenticationProvider</code>s. On successful authentication, no
	 * subsequent <code>AuthenticationProvider</code>s will be tried. If authentication
	 * was not successful by any supporting <code>AuthenticationProvider</code> the last
	 * thrown <code>AuthenticationException</code> will be rethrown.
	 * @param authentication the authentication request object.
	 * @return a fully authenticated object including credentials.
	 * @throws AuthenticationException if authentication fails.
	 */
	@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();
		for (AuthenticationProvider provider : getProviders()) {
			if (!provider.supports(toTest)) {
				continue;
			}
			if (logger.isTraceEnabled()) {
				logger.trace(LogMessage.format("Authenticating request with %s (%d/%d)",
						provider.getClass().getSimpleName(), ++currentPosition, size));
			}
			try {
				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 {
				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;
	}

	@SuppressWarnings("deprecation")
	private void prepareException(AuthenticationException ex, Authentication auth) {
		this.eventPublisher.publishAuthenticationFailure(ex, auth);
	}

	/**
	 * Copies the authentication details from a source Authentication object to a
	 * destination one, provided the latter does not already have one set.
	 * @param source source authentication
	 * @param dest the destination authentication object
	 */
	private void copyDetails(Authentication source, Authentication dest) {
		if ((dest instanceof AbstractAuthenticationToken) && (dest.getDetails() == null)) {
			AbstractAuthenticationToken token = (AbstractAuthenticationToken) dest;
			token.setDetails(source.getDetails());
		}
	}

	public List<AuthenticationProvider> getProviders() {
		return this.providers;
	}

	@Override
	public void setMessageSource(MessageSource messageSource) {
		this.messages = new MessageSourceAccessor(messageSource);
	}

	public void setAuthenticationEventPublisher(AuthenticationEventPublisher eventPublisher) {
		Assert.notNull(eventPublisher, "AuthenticationEventPublisher cannot be null");
		this.eventPublisher = eventPublisher;
	}

	/**
	 * If set to, a resulting {@code Authentication} which implements the
	 * {@code CredentialsContainer} interface will have its
	 * {@link CredentialsContainer#eraseCredentials() eraseCredentials} method called
	 * before it is returned from the {@code authenticate()} method.
	 * @param eraseSecretData set to {@literal false} to retain the credentials data in
	 * memory. Defaults to {@literal true}.
	 */
	public void setEraseCredentialsAfterAuthentication(boolean eraseSecretData) {
		this.eraseCredentialsAfterAuthentication = eraseSecretData;
	}

	public boolean isEraseCredentialsAfterAuthentication() {
		return this.eraseCredentialsAfterAuthentication;
	}

	private static final class NullEventPublisher implements AuthenticationEventPublisher {

		@Override
		public void publishAuthenticationFailure(AuthenticationException exception, Authentication authentication) {
		}

		@Override
		public void publishAuthenticationSuccess(Authentication authentication) {
		}

	}

}

ProviderManager内部维护了多个AuthenticationProvider (认证提供者),在ProviderManager中的 authenticate(Authentication authentication)方法中,每个AuthenticationProvider实例都有一次机会去校验身份认证是否成功,或者是表明自己不能做出决定并将身份认证交给后面的AuthenticationProvider实例处理。

如果所有的AuthenticationProvider实例中没有一个可以处理当前类型认证的,则会抛出ProviderNotFoundException 异常导致认证失败。ProviderNotFoundException 是一个特殊的AuthenticationException异常类型,用于说明ProviderManager没有配置支持指定类型的认证。

在这里插入图片描述

实际上,每个AuthenticationProvider实例都知道如何执行特定类型的身份验证。例如,一个AuthenticationProvider可能能够验证用户名/密码,而另一个可能能够验证SAML断言
我们只需要实现一个AuthenticationManager,并将我们所需要的 AuthenticationProvider实例配置到里面,这样就能保证我们的程序支持多种验证类型。

ProviderManager还允许配置一个可选的父级AuthenticationManager,当AuthenticationProvider实例无法执行身份验证时,可以咨询该父级AuthenticationManager。父级AuthenticationManager可以是任何类型的AuthenticationManager,但它通常是ProviderManager的一个实例。

在这里插入图片描述

实际上,多个ProviderManager实列可能拥有共同的父级AuthenticationManager。在有多个SecurityFilterChain实例的场景中比较常见,这些SecurityFilterChain实例有一些共同的身份验证(共享的父级AuthenticationManager),但也有不同的身份验证机制(不同的ProviderManager)。

在这里插入图片描述

默认情况下,尝试从成功的身份验证请求返回的对象中清除任何敏感凭据信息。 这样可以防止信息(如密码)在保留时限超过必要的时间。

4.7 AuthenticationProvider

AuthenticationProvider(认证提供者)是一个接口,其中提供两个方式,其源码如下所示:

public interface AuthenticationProvider {

	/**
	 * 
	 * @param 身份验证请求对象。
	 * @return 包括凭证的完全经过身份验证的对象。
	 * 如果AuthenticationProvider无法支持对传递的authentication对象进行身份验证,
	 * 则可能返回null。在这种情况下,将尝试下一个支持所提供的Authentication类的AuthenticationProvider。
	 * AuthenticationException 如果身份验证失败抛出异常
	 */
	Authentication authenticate(Authentication authentication) throws AuthenticationException;

	/**
	 * 
	 * @param authentication。
	 * @return 如果此AuthenticationProvider支持特定类型的Authentication对象,则返回true。
	 * 返回true并不保证AuthenticationProvider能够对呈现的Authentication类实例进行身份验证。它只是表明它可以支持对其进行更密切的评估。
	 * AuthenticationProvider仍然可以从authenticate(Authentication authentication)方法返回null,以表明应该尝试下一个支持所提供的Authentication类的AuthenticationProvider。
	 * 能够执行身份验证的AuthenticationProvider的选择是在ProviderManager运行时进行的。
	 */
	boolean supports(Class<?> authentication);

}

可以在ProviderManager中注入多个AuthenticationProvider。每个AuthenticationProvider执行特定类型的身份验证。

例如:

  • DaoAuthenticationProvider支持基于用户名/密码的身份验证。
  • JwtAuthenticationProvider支持对JWT令牌的身份验证。

Spring Security中默认提供多个ProviderManager实现类,如下图所示:

在这里插入图片描述
可以自定义扩展认证方式,例如:手机验证码等。

4.8 AuthenticationEntryPoint

AuthenticationEntryPoint(认证入口点)用于 ExceptionTranslationFilter发现认证异常时启动身份验证方案,例如:未经身份验证的请求,执行到登录页面的重定向。

AuthenticationEntryPoint有以下实现类:

在这里插入图片描述
例如:LoginUrlAuthenticationEntryPoint,会在表单登录失败或未经身份验证的请求,执行重定向(或转发)到登录表单页面的URL

4.9 AbstractAuthenticationProcessingFilter

AbstractAuthenticationProcessingFilter是一个抽象类,可以作为用户身份验证的基础,用户名/密码身份验证对应过滤器UsernamePasswordAuthenticationFilter就是继承的AbstractAuthenticationProcessingFilter

例如:自定义手机验证码认证过滤器就可以通过继承AbstractAuthenticationProcessingFilter进行开发,主要是提供认证成功、失败的相关处理:

在这里插入图片描述

①当用户提交凭证之后,AbstractAuthenticationProcessingFilter通过HttpServletRequest创建一个Authentication用于进行身份验证。创建的Authentication的具体类型依赖于AbstractAuthenticationProcessingFilter的子类。

例如UsernamePasswordAuthenticationFilter从在HttpServletRequest中提交的用户名和密码创建一个UsernamePasswordAuthenticationToken

②接着Authentication对象将被传递到AuthenticationManager中进行身份认证。

③如果认证失败:

  • SecurityContextHolder被清空。
  • RememberMeServices.loginFail被调用,如果没有配置remember-me,则不执行。
  • AuthenticationFailureHandler被调用。

④如果认证成功:

  • SessionAuthenticationStrategy收到新登录的通知 。
  • Authentication被设置到SecurityContextHolder,如果需要保存,以便在将来的请求中自动设置它,则必须显式调用SecurityContextRepository#saveContext(详见下文)。
  • RememberMeServices.loginSuccess被调用,如果没有配置remember-me,则不执行。
  • ApplicationEventPublisher发布InteractiveAuthenticationSuccessEvent事件。
  • AuthenticationSuccessHandler被调用。

http://www.kler.cn/a/146726.html

相关文章:

  • 软件设计师 - 第3章 数据结构
  • 【概率统计通俗版】极大似然估计
  • 迁移学习理论与应用
  • 【AtCoder】Beginner Contest 380-F.Exchange Game
  • leetcode:344. 反转字符串(python3解法)
  • 天童美语:下元节将至
  • 【新手解答2】深入探索 C 语言:一些常见概念的解析
  • Mysql 锁机制分析
  • Django框架环境的搭建(图文详解)
  • spring Cloud在代码中如何应用,erueka 客户端配置 和 服务端配置,Feign 和 Hystrix做高可用配置
  • IDEA DeBug
  • uniapp使用Vue3挂载函数到全局
  • 如何在gitlab上使用hooks
  • Android 相机库CameraView源码解析 (二) : 拍照
  • 深入理解MySQL索引及事务
  • python小数据分析小结及算法实践集锦
  • 后端接口返回二进制流音频数据,前端 js 如何播放?
  • 【源码】医院安全(不良)事件管理系统,11种不良事件类型,覆盖全院全部科室及所有可能发生的不良事件
  • 【nlp】3.3 Transformer论文复现:2. 编码器部分(掩码张量、多头注意力机制、前馈全连接层)
  • 给pycharm项目设置conda环境
  • Edit And Resend测试接口工具(浏览器上的Postman)
  • python+gurobi求解线性规划、整数规划、0-1规划
  • C#-关于日志的功能扩展
  • Linux的基本指令(四)
  • SpringBoot应用手册
  • docker启动容器失败,然后查看日志,docker logs查看容器出现报错: