首页 > 其他分享 >浅析Spring Security 核心组件

浅析Spring Security 核心组件

时间:2023-04-19 16:32:17浏览次数:37  
标签:Spring authentication 浅析 Authentication Security 认证 AuthenticationProvider

前言 近几天在网上找了一个 Spring Security 和JWT 的例子来学习,项目地址是: github.com/szerhusenBC… 作为学习Spring Security还是不错的,通过研究该 demo 发现自己对 Spring Security一知半解,并没有弄清楚Spring Seurity的流程,所以才想写一篇文章先来分析分析Spring Security的核心组件,其中参考了官方文档及其一些大佬写的Spring Security分析文章,有雷同的地方还请见谅。

Spring Security的核心类 Spring Security的核心类主要包括以下几个:

SecurityContextHolder: 存放身份信息的容器

Authentication: 身份信息的抽象接口

AuthenticationManager: 身份认证器,认证的核心接口

UserDetailsService: 一般用于从数据库中加载身份信息

UserDetails: 相比Authentication,有更详细的身份信息

SecurityContextHolder、Securityontext和Authentication SecurityContextHolder用于存储安全上下文(security context)的信息,即一个存储身份信息,认证信息等的容器。SecurityContextHolder默认使用 ThreadLocal策略来存储认证信息,即一种与线程绑定的策略,每个线程执行时都可以获取该线程中的 安全上下文(security context),各个线程中的安全上下文互不影响。而且如果说要在请求结束后清除安全上下文中的信息,利用该策略Spring Security也可以轻松搞定。

因为身份信息时与线程绑定的,所以我们可以在程序的任何地方使用静态方法获取用户信息,一个获取当前登录用户的姓名的例子如下:

Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();

if (principal instanceof UserDetails) { String username = ((UserDetails)principal).getUsername(); } else { String username = principal.toString(); } getAuthentication()方法返回了认证信息,准确的说是一个 Authentication实例,Authentication是 Spring Security 中的一个重要接口,直接继承自 Principal类,该接口表示对用户身份信息的抽象,接口源码如下:

public interface Authentication extends Principal, Serializable { //权限信息列表,默认是 GrantedAuthority接口的一些实现Collection<? extends GrantedAuthority> getAuthorities(); //密码信息,用户输入的密码字符串,认证后通常会被移除,用于保证安全Object getCredentials();//细节信息,web应用中通常的接口为 WebAuthenticationDetails,它记录了访问者的ip地址和sessionId的值Object getDetails();//身份信息,返回UserDetails的实现类Object getPrincipal();//认证状态,默认为false,认证成功后为 trueboolean isAuthenticated();//上述身份信息是否经过身份认证 void setAuthenticated(boolean var1) throws IllegalArgumentException; } AuthenticationManager、ProviderManager 和 AuthenticationProvider AuthenticationManager是身份认证器,认证的核心接口,接口源码如下:

public interface AuthenticationManager { /** * Attempts to authenticate the passed {@link Authentication} object, returning a * fully populated Authentication object (including granted authorities) * @param authentication the authentication request object * * @return a fully authenticated object including credentials * * @throws AuthenticationException if authentication fails */ Authentication authenticate(Authentication authentication) throws AuthenticationException; } 该接口只有一个 authenticate()方法,用于身份信息的认证,如果认证成功,将会返回一个带了完整信息的Authentication,在之前提到的Authentication所有的属性都会被填充。

在Spring Security中,AuthenticationManager默认的实现类是 ProviderManager,ProviderManager并不是自己直接对请求进行验证,而是将其委派给一个 AuthenticationProvider列表。列表中的每一个 AuthenticationProvider将会被依次查询是否需要通过其进行验证,每个 provider的验证结果只有两个情况:抛出一个异常或者完全填充一个 Authentication对象的所有属性。ProviderManager中的部分源码如下:

public class ProviderManager implements AuthenticationManager, MessageSourceAware, InitializingBean {

//维护一个AuthenticationProvider 列表
private List<AuthenticationProvider> providers = Collections.emptyList();
private AuthenticationManager parent;
//构造器,初始化 AuthenticationProvider 列表
public ProviderManager(List<AuthenticationProvider> providers) {
	this(providers, null);
}
public ProviderManager(List<AuthenticationProvider> providers,
		AuthenticationManager parent) {
	Assert.notNull(providers, "providers list cannot be null");
	this.providers = providers;
	this.parent = parent;
	checkState();
}
public Authentication authenticate(Authentication authentication)
		throws AuthenticationException {
	Class<? extends Authentication> toTest = authentication.getClass();
	AuthenticationException lastException = null;
	Authentication result = null;
	boolean debug = logger.isDebugEnabled();// AuthenticationProvider 列表中每个Provider依次进行认证
	for (AuthenticationProvider provider : getProviders()) {
		if (!provider.supports(toTest)) {
			continue;
		}...
		try { 
		//调用 AuthenticationProvider 的 authenticate()方法进行认证
			result = provider.authenticate(authentication);
			if (result != null) {
				copyDetails(authentication, result);
				break;
			}
		}
		...
		catch (AuthenticationException e) {
			lastException = e;
		}
	}// 如果 AuthenticationProvider 列表中的Provider都认证失败,且之前有构造一个 AuthenticationManager 实现类,那么利用AuthenticationManager 实现类 继续认证
	if (result == null && parent != null) {
		// Allow the parent to try.
		try {
			result = parent.authenticate(authentication);
		}...
		catch (AuthenticationException e) {
			lastException = e;
		}
	}//认证成功
	if (result != null) {
		if (eraseCredentialsAfterAuthentication
				&& (result instanceof CredentialsContainer)) {
			// Authentication is complete. Remove credentials and other secret data
			// from authentication
			//成功认证后删除验证信息
			((CredentialsContainer) result).eraseCredentials();
		}//发布登录成功事件
		eventPublisher.publishAuthenticationSuccess(result);
		return result;
	}

	// 没有认证成功,抛出一个异常
	if (lastException == null) {
		lastException = new ProviderNotFoundException(messages.getMessage(
				"ProviderManager.providerNotFound",
				new Object[] { toTest.getName() },
				"No AuthenticationProvider found for {0}"));
	}
	prepareException(lastException, authentication);
	throw lastException;
}

ProviderManager中的 authenticationManager列表依次去尝试认证,认证成功即返回,认证失败返回null,如果所有的 Provider都认证失败, ProviderManager将会抛出一个 ProviderNotFoundException异常。

事实上,AuthenticationProvider是一个接口,接口定义如下:

public interface AuthenticationProvider {//认证方法 Authentication authenticate(Authentication authentication) throws AuthenticationException;//该Provider是否支持对应的Authentication boolean supports(Class<?> authentication); } 在 ProviderManager的 Javadoc曾提到,

If more than one AuthenticationProvider supports the passed Authentication object, the first one able to successfully authenticate the Authentication object determines the result, overriding any possible AuthenticationException thrown by earlier supporting AuthenticationProvider s. On successful authentication, no subsequent AuthenticationProvider s will be tried. If authentication was not successful by any supporting AuthenticationProvider the last thrown AuthenticationException will be rethrown 大致意思是:

如果有多个 AuthenticationProvider 都支持同一个Authentication 对象,那么 第一个 能够成功验证Authentication的 Provder 将填充其属性并返回结果,从而覆盖早期支持的 AuthenticationProvider抛出的任何可能的 AuthenticationException。一旦成功验证后,将不会尝试后续的 AuthenticationProvider。如果所有的 AuthenticationProvider都没有成功验证 Authentication,那么将抛出最后一个Provider抛出的AuthenticationException。(AuthenticationProvider可以在Spring Security配置类中配置) PS:

当然有时候我们有多个不同的 AuthenticationProvider,它们分别支持不同的 Authentication对象,那么当一个具体的 AuthenticationProvier传进入 ProviderManager的内部时,就会在 AuthenticationProvider列表中挑选其对应支持的provider对相应的 Authentication对象进行验证。 不同的登录方式认证逻辑是不一样的,即 AuthenticationProvider会不一样,如果使用用户名和密码登录,那么在Spring Security 提供了一个 AuthenticationProvider的简单实现 DaoAuthenticationProvider,这也是框架最早的 provider,它使用了一个 UserDetailsService来查询用户名、密码和 GrantedAuthority,一般我们要实现UserDetailsService接口,,并在Spring Security配置类中将其配置进去,这样也促使使用DaoAuthenticationProvider进行认证,然后该接口返回一个UserDetails,它包含了更加详细的身份信息,比如从数据库拿取的密码和权限列表,AuthenticationProvider 的认证核心就是加载对应的 UserDetails来检查用户输入的密码是否与其匹配,即UserDetails和Authentication两者的密码(关于 UserDetailsService和UserDetails的介绍在下面小节介绍。)。而如果是使用第三方登录,比如QQ登录,那么就需要设置对应的 AuthenticationProvider,这里就不细说了。

认证成功后清除验证信息 在上面ProviderManager的源码中我还发现一点,在认证成功后清除验证信息,如下:

if (eraseCredentialsAfterAuthentication && (result instanceof CredentialsContainer)) { // Authentication is complete. Remove credentials and other secret data // from authentication //成功认证后删除验证信息 ((CredentialsContainer) result).eraseCredentials(); } 从 spring Security 3.1之后,在请求认证成功后 ProviderManager将会删除 Authentication中的认证信息,准确的说,一般删除的是 密码信息,这可以保证密码的安全。我跟了一下源码,实际上执行删除操作的步骤如下:

public class UsernamePasswordAuthenticationToken extends AbstractAuthenticationToken {public void eraseCredentials() {super.eraseCredentials();//使密码为nullthis.credentials = null;} } public abstract class AbstractAuthenticationToken implements Authentication, CredentialsContainer { ... public void eraseCredentials() {//擦除密码this.eraseSecret(this.getCredentials());this.eraseSecret(this.getPrincipal());this.eraseSecret(this.details); }

private void eraseSecret(Object secret) {if (secret instanceof CredentialsContainer) {((CredentialsContainer)secret).eraseCredentials();} } } 从源码就可以看出实际上就是擦除密码操作。

UserDetailsService 和 UserDetails UserDetailsService简单说就是加载对应的UserDetails的接口(一般从数据库),而UserDetails包含了更详细的用户信息,定义如下:

public interface UserDetails extends Serializable { Collection<? extends GrantedAuthority> getAuthorities(); String getPassword(); String getUsername(); boolean isAccountNonExpired(); boolean isAccountNonLocked(); boolean isCredentialsNonExpired(); boolean isEnabled(); } UserDetails 接口与 Authentication接口相似,它们都有 username、authorities。它们的区别如下:

Authentication 的 getCredentials() 与 UserDetails 中的 getPassword() 不一样,前者是用户提交的密码凭证,后者是用户正确的密码,(一般是从数据库中载入的密码),AuthenticationProvider就会对两者进行对比。

Authentication 中的 getAuthorities() 实际上是由 UserDetails 的 getAuthorities()传递形成的。

Authentication 中的 getUserDetails() 中的 UserDetails 用户详细信息时经过 AuthenticationProvider认证之后填充的。

认证过程样本示例 下面来看一个官方文档提供的例子,代码如下:

public class SpringSecuriryTestDemo {private static AuthenticationManager am = new SampleAuthenticationManager();public static void main(String[] args) throws IOException {BufferedReader in = new BufferedReader(new InputStreamReader(System.in));while (true) {System.out.println("Please enter your username:");String name = in.readLine();System.out.println("Please enter your password:");String password = in.readLine();try {Authentication request = new UsernamePasswordAuthenticationToken(name, password);Authentication result = am.authenticate(request);SecurityContextHolder.getContext().setAuthentication(request);break;} catch (AuthenticationException e) {System.out.println("Authentication failed: " + e.getMessage());}}System.out.println("Successfully authenticated. Security context contains: " + SecurityContextHolder.getContext().getAuthentication());}static class SampleAuthenticationManager implements AuthenticationManager {static final List();static {AUTHORITIES.add(new SimpleGrantedAuthority("ROLE_USER"));}@Overridepublic Authentication authenticate(Authentication authentication) throws AuthenticationException {if (authentication.getName().equals(authentication.getCredentials())) {return new UsernamePasswordAuthenticationToken(authentication.getName(), authentication.getCredentials(), AUTHORITIES);}throw new BadCredentialsException("Bad Credentials");}} } 测试如下:

Please enter your username: pjmike Please enter your password: 123 Authentication failed: Bad Credentials Please enter your username: pjmike Please enter your password: pjmike Successfully authenticated. Security context contains: org.springframework.security.authentication.UsernamePasswordAuthenticationToken@441d0230: Principal: pjmike; Credentials: [PROTECTED]; Authenticated: true; Details: null; Granted Authorities: ROLE_USER 上面的例子很简单,不是源码,只是为了演示认证过程编写的Demo,而且也缺少过滤器链,但是麻雀虽小,五脏俱全,基本包括了Spring Security的核心组件,表达了Spring Security 认证的基本思想。解读一下:

用户名和密码被封装到 UsernamePasswordAuthentication的实例中(该类是 Authentication接口的实现)

该 Authentication传递给 AuthenticationManager进行身份验证

认证成功后,AuthenticationManager会返回一个完全填充的 Authentication实例,该实例包含权限信息,身份信息,细节信息,但是密码通常会被移除

通过调用 SecurityContextHolder.getContext().setAuthentication(…)传入上面返回的填充了信息的 Authentication对象

通过上面一个简单示例,我们大致明白了Spring Security的基本思想,但是要真正理清楚Spring Security的认证流程这还不够,我们需要深入源码去探究,后续文章会更加详细的分析Spring Security的认证过程。

小结 这篇文章主要分析了Spring Security的一些核心组件,参考了官方文档及其相关译本,对核心组件有一个基本认识后,才便于后续更加详细的分析Spring Security的认证过程。

标签:Spring,authentication,浅析,Authentication,Security,认证,AuthenticationProvider
From: https://blog.51cto.com/u_15668812/6206705

相关文章

  • springboot学习之五(自动配置)
    一、@Conditional源码springboot的自动配置是通过@Conditional注解(条件判断)实现的.@Conditional是Spring4版本新提供的一种注解,它的作用是按照设定的条件进行判断,把满足判断条件的bean注册到Spring容器。packageorg.springframework.context.annotation;importjava.lang.a......
  • IDEA Spring Boot项目的依赖入库问题
    SpringBoot项目在创建的时候,尽量把需要的依赖通过官网选择器勾选开发的依赖框架后期在开发过程中,可以通过点击当前版本的boot-pom的依赖看其支持的依赖的版本,手动进行导入或通过.pom的文件下右键generate,通过editstarter再次进入官网选择器进行依赖的选择和删除重新对ma......
  • 创建idea springboot(spring Initializr项目)需要手动导包)
    用注解@RestController 的时候,报错:找不到符号原来是没有import这个包,复制粘贴 @RestController 的时候,idea并没有自动import这个包,导致找不到,要手动import。手动导入就行了。另外,如果是创建的maven项目,由于main类文件的放置位置不同,也会报错:nested exception is java.lang.I......
  • @SpringBootApplication等四个爆红
    在黑马的上面学习,按步骤做,出现爆红问题,之后尝试过很多方法,后发现没导包。导包后可以 ......
  • Java SpringBoot 加载 yml 配置文件中字典项
    将字典数据,配置在yml文件中,通过加载yml将数据加载到Map中SpringBoot中yml配置、引用其它yml中的配置。#在配置文件目录(如:resources)下新建application-xxx必须以application开头的yml文件,多个文件用","号分隔,不能换行项目结构文件application.ymlserver:po......
  • 浅析ReentrantLock和AQS
          AQS的全称是AbstractQueuedSynchronizer,这是AQS框架的核心抽象类。ReentrantLock有三个内部类:Sync、NonfairSync、FairSync。FairSync代表了公平锁,NonfairSync代表了非公平锁,NonfairSync和FairSync都继承自Sync,Sync继承自AbstractQueuedSynchronizer。      AQ......
  • 提高kafka消费速度之从源码去了解Spring-kafka的concurrency参数
    网上看到这篇文章,觉得很不错,这里转载记录一下。转自:提高kafka消费速度之从源码去了解Spring-kafka的concurrency参数-简书第一部分、引言    在spring应用中,如果我们需要订阅kafka消息,通常情况下我们不会直接使用kafka-client,而是使用了更方便的一层封装spring-kafk......
  • 5.数据交换格式与 SpringIOC 底层实现
    数据交换格式与SpringIOC底层实现一、课程目标XML和JSONJava反射机制手写SpringIOC二、什么是数据交换格式客户端与服务器常用数据交换格式xml、json、html三、数据交换格式用场景移动端(安卓、IOS)通讯方式采用http协议+JSON格式走restful风格。很多互......
  • 14.SpringAOP 编程
    SpringAOP编程课程目标代理设计模式Spring的环境搭建SpringIOC与AOPSpring事物与传播行为一、代理模式1.1概述代理(Proxy)是一种设计模式,提供了对目标对象另外的访问方式;即通过代理访问目标对象。这样好处:可以在目标对象实现的基础上,增强额外的功能操作。(扩......
  • 浅析python中的生成器和迭代器
    一、什么叫生成器?在Python中,一边循环一边计算的机制,称为生成器:generator二、怎么创建生成器1.生成器表达式()生成器表达式返回一个生成器对象,需要用一个变量名来接收g=(x*3forxinrange(5))#打印g,返回一个生成器对象print(g)#<generatorobject<genexpr>at0x000......