【深入浅出 Spring Security(四)】登录用户数据的获取,超详细的源码分析

news2024/12/26 11:17:32

登录用户数据的获取

  • 一、SecurityContextHolder 源码分析
    • ListeningSecurityContextHolderStrategy 使用案例
    • SecurityContextPersistenceFilter 说明
  • 二、登录用户数据的获取
  • 三、总结

在【深入浅出Spring Security(一)】Spring Security的整体架构 中叙述过一个SecurityContextHolder 这个类。说在处理请求时,Spring Security 会先从 Session 中取出用户登录数据,保存到 SecurityContextHolder 中,然后在请求处理完毕后,又会拿 SecurityContextHolder 中的数据保存到 Session 中,然后再清空 SecurityContextHolder 中的数据。且说了 SecurityContextHolder 内部数据保存默认是通过 ThreadLocal 来实现的。

下面分析 SecurityContextHolder 的源码,并述说如何在代码中获取登录用户的数据。
(如果不想看源码分析的可以直接跳过看怎么获取用户数据)

一、SecurityContextHolder 源码分析

在分析源码之前,可以看一下下面这个图,它展示了 SecurityContextHolder 和 用户数据信息 的结构关系。SecurityContextHolder 依赖 SecurityContext,SecurityContext 封装了 Authentication,而Authentication 即是我们所指的认证后的用户数据信息。(从这关系以及上面的分析,大概应该可以猜测到 SecurityContextHolder 中用了策略设计模式,命名也都很规范化,~ Context,~ ContextHolder🤣)
在这里插入图片描述

策略模式(Strategy):它定义了算法家族,分别封装起来,让它们之间可以互相替换,此模式让算法的变化,不会影响到使用算法的客户。

既然说它用了策略模式,那SecurityContextHolder中定义的算法家族呢?下面来看一下SecurityContextHolder类中的属性。

public class SecurityContextHolder {
	// 指的是算法策略中的ThreadLocalSecurityContextHolderStrategy
	public static final String MODE_THREADLOCAL = "MODE_THREADLOCAL";
	//InheritableThreadLocalSecurityContextHolderStrategy
	public static final String MODE_INHERITABLETHREADLOCAL = "MODE_INHERITABLETHREADLOCAL";
	// GlobalSecurityContextHolderStrategy
	public static final String MODE_GLOBAL = "MODE_GLOBAL";
	// 这个表示不适用任何策略,用原先的HttpSession
	private static final String MODE_PRE_INITIALIZED = "MODE_PRE_INITIALIZED";
	// 配置名称
	public static final String SYSTEM_PROPERTY = "spring.security.strategy";
	// 首先是从系统配置中获取
	// idea中可以在vmoptions中进行配置,
	// 例如:-Dspring.security.strategy=MODE_THREADLOCAL
	private static String strategyName = System.getProperty(SYSTEM_PROPERTY);

	private static SecurityContextHolderStrategy strategy;
}

可以看见有一个 SecurityContextHolderStrategy 对象 strategy,它就是“算法的封装体”。SecurityContextHolderStrategy 是一个接口,下面是其源代码,比较简单。

public interface SecurityContextHolderStrategy {
	// 清除SecurityContext
	void clearContext();
	// 获取SecurityContext
	SecurityContext getContext();
	// 存取SecurityContext
	void setContext(SecurityContext context);
	// 得到一个空的SecurityContext
	SecurityContext createEmptyContext();

}

看下图可以知道算法家族的成员。

在这里插入图片描述

  • ThreadLocalSecurityContextHolderStrategy:存储数据的载体是一个 ThreadLocal,所以针对 SecurityContext 的清空、获取以及存储,都是在 ThreadLocal 中进行操作。源码过于简单,不分析了,自己看吧。
    在这里插入图片描述

  • InheritableThreadLocalSecurityContextHolderStrategy:和前者实现策略没有区别,只不过用的是ThreadLocal的子类InheritableThreadLocal,这样子线程和父线程都可以获取到用户数据了。源码也没啥,自己看看就OK了。
    在这里插入图片描述

  • GlobalSecurityContextHolderStrategy:它实现起来就更更更简单了,直接用个静态变量保存 SecurityContext,所以多线程环境下它是可以使用了,但一般在web开发中,这肯定是使用的少的。
    在这里插入图片描述

  • ListeningSecurityContextHolderStrategy:SecurityContext 的事件监听策略,它是5.6版本后推出来放到SecurityContextHolderStrategy 策略中的。《深入浅出 Spring Security》书中并没有提到它,但我还是有必要了解的。使用它可以在不去配置系统配置的情况下更换策略,也可以监听 SecurityContext 的创建和销毁事件。注意这里没有获取事件。

    • 它构造方法进行了重载,可以看一下(有些构造源码上说5.7更新的,不管了,现在都 6点 多了),一些判断是否为空的代码我就去调了,留核心代码。
public final class ListeningSecurityContextHolderStrategy implements SecurityContextHolderStrategy {

	// 监听器集合
	private final Collection<SecurityContextChangedListener> listeners;
	// 委托策略对象,默认的话也是ThreadLocalSecurityContextHolderStrategy
	private final SecurityContextHolderStrategy delegate;

	public ListeningSecurityContextHolderStrategy(Collection<SecurityContextChangedListener> listeners) {
		this(new ThreadLocalSecurityContextHolderStrategy(), listeners);
	}


	public ListeningSecurityContextHolderStrategy(SecurityContextChangedListener... listeners) {
		this(new ThreadLocalSecurityContextHolderStrategy(), listeners);
	}


	public ListeningSecurityContextHolderStrategy(SecurityContextHolderStrategy delegate,
			Collection<SecurityContextChangedListener> listeners) {
		this.delegate = delegate;
		this.listeners = listeners;
	}

// 可变参数重载,可进行配置自己想要的策略对象(delegate)
	public ListeningSecurityContextHolderStrategy(SecurityContextHolderStrategy delegate,
			SecurityContextChangedListener... listeners) {
		this.delegate = delegate;
		this.listeners = Arrays.asList(listeners);
	}

在看看它的其他源码,在创建和销毁 SecurityContext 的时候会调用监听器去监听。


	@Override
	public void clearContext() {
		SecurityContext from = getContext();
		this.delegate.clearContext();
		publish(from, null);
	}
	
	@Override
	public SecurityContext getContext() {
		return this.delegate.getContext();
	}

	@Override
	public void setContext(SecurityContext context) {
		SecurityContext from = getContext();
		this.delegate.setContext(context);
		publish(from, context);
	}

	@Override
	public SecurityContext createEmptyContext() {
		return this.delegate.createEmptyContext();
	}
	// 执行监听措施
	private void publish(SecurityContext previous, SecurityContext current) {
		if (previous == current) {
			return;
		}
		SecurityContextChangedEvent event = new SecurityContextChangedEvent(previous, current);
		for (SecurityContextChangedListener listener : this.listeners) {
			listener.securityContextChanged(event);
		}
	}

监听器SecurityContextChangedEvent 是一个函数式接口,咱配置的时候直接使用 lambda 就好了。

讲了半天的策略,回归策略的封装者 SecurityContextHolder。来看看它的初始化操作,它是提供了一个静态代码块,执行初始化。

	static {
		initialize();
	}

	private static void initialize() {
		initializeStrategy();
		initializeCount++;
	}

	private static void initializeStrategy() {
	// 首先判断是否是不使用策略
		if (MODE_PRE_INITIALIZED.equals(strategyName)) {
			Assert.state(strategy != null, "When using " + MODE_PRE_INITIALIZED
					+ ", setContextHolderStrategy must be called with the fully constructed strategy");
			return;
		}
		// 然后判断是否为空,为空就默认设置为ThreadLocalSecurity...
		if (!StringUtils.hasText(strategyName)) {
			// Set default
			strategyName = MODE_THREADLOCAL;
		}
		// 这后面就一系列的判断没啥。
		if (strategyName.equals(MODE_THREADLOCAL)) {
			strategy = new ThreadLocalSecurityContextHolderStrategy();
			return;
		}
		if (strategyName.equals(MODE_INHERITABLETHREADLOCAL)) {
			strategy = new InheritableThreadLocalSecurityContextHolderStrategy();
			return;
		}
		if (strategyName.equals(MODE_GLOBAL)) {
			strategy = new GlobalSecurityContextHolderStrategy();
			return;
		}
		// Try to load a custom strategy
		try {
		// 如果以上都没匹配到的话,就默认使用的是类的全路径引出策略
		// 通过反射去构造
			Class<?> clazz = Class.forName(strategyName);
			Constructor<?> customStrategy = clazz.getConstructor();
			strategy = (SecurityContextHolderStrategy) customStrategy.newInstance();
		}
		catch (Exception ex) {
			ReflectionUtils.handleReflectionException(ex);
		}
	}

了解了其如何进行初始化的后,那就好办了,直接看它内部方法吧。其内部方法都是静态的。

// 清除SecurityContext
public static void clearContext() {
		strategy.clearContext();
	}
// 获取SecurityContext
	public static SecurityContext getContext() {
		return strategy.getContext();
	}

// 初始化次数,emmm,发送请求的次数?
	public static int getInitializeCount() {
		return initializeCount;
	}

// 配置SecurityContext
	public static void setContext(SecurityContext context) {
		strategy.setContext(context);
	}

// 配置StrategyName
	public static void setStrategyName(String strategyName) {
		SecurityContextHolder.strategyName = strategyName;
		initialize();
	}
// 出于5.6版本,估计是让你更好的配置监听策略用的,事实上也就这个方法可以做到了
	public static void setContextHolderStrategy(SecurityContextHolderStrategy strategy) {
		Assert.notNull(strategy, "securityContextHolderStrategy cannot be null");
		SecurityContextHolder.strategyName = MODE_PRE_INITIALIZED;
		SecurityContextHolder.strategy = strategy;
		initialize();
	}
// 获取策略对象
	public static SecurityContextHolderStrategy getContextHolderStrategy() {
		return strategy;
	}

// 创建空的SecurityContext
// 也就是创建SecurityContextImpl
	public static SecurityContext createEmptyContext() {
		return strategy.createEmptyContext();
	}

源码分析到这,差不多就很清晰了,再看看SecurityContextImpl的源码吧,其实不用看也知道,就是 Authentication 对象的封装,这看一下属性和构造就差不多可以猜到大概了。在这里插入图片描述
最后还需要注意:ThreadLocalSecurityContextHolderStrategy、InheritableThreadLocalSecurityContextHolderStrategy、GlobalSecurityContextHolderStrategy 访问权限都是default默认的,不是本包下的是不让new的,也就是对外不让实例化,你只能通过它给的进行对内策略更改。

ListeningSecurityContextHolderStrategy 使用案例

@Component
@Slf4j
public class InitCommandRun implements CommandLineRunner {
    @Override
    public void run(String... args) throws Exception {
// 配置 InheritableThreadLocalSecurityContextHolderStrategy        
SecurityContextHolder.setStrategyName(SecurityContextHolder.MODE_INHERITABLETHREADLOCAL);
// 去获取这个策略对象
        SecurityContextHolderStrategy initStrategy = SecurityContextHolder.getContextHolderStrategy();
        // 将获取到的策略对象用到监听策略中,当委托策略
        SecurityContextHolderStrategy strategy = new ListeningSecurityContextHolderStrategy(
                initStrategy,
                event -> {
            if(event.getNewContext() != null)
                log.warn("new context->{}",event.getNewContext());
        });
        SecurityContextHolder.setContextHolderStrategy(strategy);
    }
}

测试结果

请添加图片描述

SecurityContextPersistenceFilter 说明

Persistence(持久性)。

在【深入浅出Spring Security(二)】Spring Security的实现原理 中概述了Spring Security 中默认加载的过滤器,SecurityContextPersistenceFilter 即是其中的一员。它的作用是为了存储 SecurityContext 而设计的。

它整体来说做了两件事:

  • 当一个请求到来时,从 HttpSession 中获取 SecurityContext 并存入 SecurityContextHolder 中,这样在同一个请求的后续处理过程中,开发者始终可以通过 SecurityContextHolder 获取到当前登录用户信息。
  • 当一个请求处理完毕时,从 SecurityContextHolder 中获取 SecurityContext 并存入 HttpSession 中(主要针对异步 Servlet,不是异步的相应提交自动就会保存到HttpSession中),方便下一个请求到来时,再从 HttpSession 中拿出来使用,同时擦除 SecurityContextHolder 中的登录用户信息。

下面是 SecurityContextPersistenceFilter 过滤器的核心代码(下面出现的 repo 是 SecurityContextRepository 对象,默认是HttpSessionSecurityContextRepository对象):

	private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
			throws IOException, ServletException {
		// 获取 SecurityContext 对象
		HttpRequestResponseHolder holder = new HttpRequestResponseHolder(request, response);
		SecurityContext contextBeforeChainExecution = this.repo.loadContext(holder);
		try {
		// 存入到 SecurityContextHolder 中
			SecurityContextHolder.setContext(contextBeforeChainExecution);
			// 让下一个过滤器处理请求
			chain.doFilter(holder.getRequest(), holder.getResponse());
		}
		finally {
		// 请求结束后清楚SecurityContextHolder 中的用户信息
		// 并把信息保存在HttpSession中
			SecurityContext contextAfterChainExecution = SecurityContextHolder.getContext();
			// Crucial removal of SecurityContextHolder contents before anything else.
			SecurityContextHolder.clearContext();
			this.repo.saveContext(contextAfterChainExecution, holder.getRequest(), holder.getResponse());
			request.removeAttribute(FILTER_APPLIED);
		}
	}

注意:SecurityContextPersistenceFilter被标记为已过时(Deprecated),但它仍然被包含在Spring Security默认的过滤器链中。这是因为虽然存在一些问题,但它仍然是一个广泛使用的过滤器,并且在某些情况下仍然是有用的。新版本是去拿 PersistentTokenBasedRememberMeServices 去取代它。

二、登录用户数据的获取

通过上面的源码分析呢?咱可以知道如何获取用户信息了(Authentication)。
调用 SecurityContextHolder 中的 getContext() 静态方法获取其对应策略中保存的 SecurityContext 对象,再调用 getAuthentication() 方法获取 Authentication 对象。

@RestController
public class TestController {

    @GetMapping("/test")
    public Object test(){
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        User user = (User) authentication.getPrincipal();
        /*return "Spring Security Test Success!";*/
        return user;
    }

}

getPrincipal() 是去获取主要的用户信息,它是一个User对象,所以可以进行强转。

测试效果

请添加图片描述
由于我做了如下配置,所以即使在多线程情况下,也是可以使用的(子线程可以用父线程中的 SecurityContext)。

在这里插入图片描述

三、总结

  • SecurityContextPersistenceFilter 完成了 SecurityContext 的存储和擦除;
  • 在 5.6 版本(准确来说是5.7)后引入了 ListeningSecurityContextHolderStrategy 监听SecurityContext策略;
  • 可以使用 SecurityContextHolder.getContext.getAuthentication() 的方式获取登录用户数据;
  • SecurityContext 的存储和擦除内部用了策略设计模式,SecurityContextHolder 中定义了 SecurityContextHolderStrategy 策略,去获取、擦除、存储SecurityContext。

当使用 ListeningSecurityContextHolderStrategy 时,可以向如下这样使用。当然它默认的执行策略是 ThreadLocalSecurity… ,所以当不需要换策略的话直接用监听器对象当构造参数构造即可,如果想切换成多线程,就像如下那样配置吧。

在这里插入图片描述当然也可以去配置idea的vmoptions参数,但小编并不觉得它是个好主意。你觉得呢?

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/603922.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Gradle 介绍,根据 Gradle 官方文档整理

这部分内容主要根据 Gradle 官方文档整理&#xff0c;做了对应的删减&#xff0c;主要保留比较重要的部分&#xff0c;不涉及实战&#xff0c;主要是一些重要概念的介绍。 Gradle 这部分内容属于可选内容&#xff0c;可以根据自身需求决定是否学习&#xff0c;目前国内还是使用…

回调函数与钩子函数的区别,另QT中connect函数的实现,lambda的使用

1、钩子函数是回调函数的一种 广泛来说两者都是一样的 严格来说 钩子函数的函数名早已被定义好&#xff0c;只是函数内部需要用户在应用层来定义&#xff0c; 1&#xff09;可以完全通过宏来实现系统是否调用该函数&#xff08;底层不封闭&#xff0c;修改宏的参数实现是否编…

chatgpt赋能python:Python动态分配内存:了解它的工作原理

Python动态分配内存&#xff1a;了解它的工作原理 Python是一种高级编程语言&#xff0c;它在处理内存和垃圾回收方面具有独特的方式。在Python中&#xff0c;内存分配和释放是动态的&#xff0c;并且由解释器自动完成。这意味着&#xff0c;Python程序员无需手动管理内存&…

C/C++/Qt 文件操作 效率比较

C/C/Qt 文件操作 & 效率比较 1 介绍2 比较结果2.1 Linux平台上运行程序普遍比Windows上快&#xff1b;Windows下VC编译的程序一般运行比MINGW&#xff08;MINimal Gcc for Windows&#xff09;快2.2 二进制文件的操作要快于文本文件&#xff1b;写文件的操作要快于读文件&a…

chatgpt赋能python:Python剔除函数的使用介绍

Python剔除函数的使用介绍 在Python编程中&#xff0c;剔除函数是非常有用的工具&#xff0c;它可以帮助程序员快速筛选出不符合条件的数据。本文将介绍剔除函数的概念和常见用法&#xff0c;以及如何有效使用剔除函数解决实际问题。 剔除函数的概念 剔除函数是指Python中的…

反射相关知识点

这里写目录标题 反射概述获取Class对象的三种方式总结具体代码演示 获取构造方法以及构造方法里的信息利用Class对象调用对应方法&#xff0c;以及用调取出来的Constructor&#xff08;构造器类&#xff09;创建对象具体代码 获取成员变量利用Class对象调用对应方法&#xff0c…

chatgpt赋能python:Python加速读取CSV文件的方法

Python加速读取CSV文件的方法 介绍 CSV文件是一种常见的数据格式&#xff0c;因为其简单和易于理解&#xff0c;被广泛应用于数据处理和数据分析。然而&#xff0c;在处理大型CSV文件时&#xff0c;读取速度会成为问题。Python作为一种高级编程语言&#xff0c;具有易学易用的…

AI+是企业管理软件的下一站和终点站

作为GPT综合症的表现&#xff0c;准备陆续写一点关于AI的文章。就从这一篇开始吧。 这篇文章原来是在2019年1月份我发在新浪微博和LinkedIn上的。刚搜了一下&#xff0c;全然不见了踪影。原因大家也都知道。但是&#xff0c;我想那个思想的小火花一定还在&#xff0c;在某处酝…

Jenkins概念及安装配置教程(三)

如何配置Jenkins&#xff1f; Jenkins 中的用户管理 要在 Jenkins 中管理用户&#xff0c;您应该导航到管理 Jenkins &#x1f86a; 配置全局安全。理想的选择是让 Jenkins 拥有自己的用户数据库。您可以创建一个只有读取权限的匿名用户。为您打算在下一步中添加的用户创建条…

【ARMv8 SIMD和浮点指令编程】NEON 加法指令——加法都能玩出花

向量加法包括常见的普通加指令&#xff0c;还包括长加、宽加、半加、饱和加、按对加、按对加并累加、选择高半部分结果加、全部元素加等。如果你和我一开始以为的只有一种普通加&#xff0c;那就太小看设计者了&#xff01;同时这么多加法指令的确会提升我们设计程序的效率&…

numpy包中的编码函数和解码函数numpy.char.encode() numpy.char.decode()

【小白从小学Python、C、Java】 【计算机等考500强证书考研】 【Python-数据分析】 numpy包中的编码函数和解码函数 numpy.char.encode() numpy.char.decode() [太阳]选择题 下列代码最后输出的结果是&#xff1f; import numpy as np x np.array([I, Love, Python]) print(…

Python jieba库

前言 Jieba库是优秀的中文分词第三方库&#xff0c;中文文本需要通过分词获得单个的词语。 Jieba库的分词原理&#xff1a;利用一个中文词库&#xff0c;确定汉字之间的关联概率&#xff0c;汉字间概率大的组成词组&#xff0c;形成分词结果。除了分词&#xff0c;用户还可以…

【数据库原理与应用 - 第八章】数据库的事务管理与并发控制

目录 一、事务管理 1、概念及特性 2、事务控制 &#xff08;1&#xff09;事务控制语句 显示事务举例 二、并发控制 1、问题引入 2、并发执行带来的问题 &#xff08;1&#xff09;丢失修改 &#xff08;2&#xff09;不可重复读 &#xff08;3&#xff09;读"…

[自学记录02|百人计划]纹理压缩

一、什么是纹理压缩 纹理压缩是为了解决内存、带宽问题&#xff0c;专为在计算机图形渲染系统中存储纹理而使用的图像压缩技术。 1.图片格式和纹理格式的区别 (1)图片格式 图片格式是图片文件的存储格式&#xff0c;通常在磁盘、内存中储存和传输文件时使用&#xff1b;例如…

单片机GD32F303RCT6 (Macos环境)开发 (三十三)—— 光照传感器 (BH1750)

GD32 光照传感器 BH1750的使用 1、GPIO模拟i2c配置 使用管脚为SCL PB10 SDA PB11&#xff0c;移植代码时可换自己的管脚。软件模拟i2c在十九章中讲过&#xff0c;与其不同的地方是&#xff0c;这里的us延时函数&#xff0c;换成了定时器3做us级的延时。 tim3的配置&#xf…

linux 找回root密码(CentOS7.6)

linux 找回root密码(CentOS7.6) 首先&#xff0c;启动系统&#xff0c;进入开机界面&#xff0c;在界面中按“e”进入编辑界面。如图 2. 进入编辑界面&#xff0c;使用键盘上的上下键把光标往下移动&#xff0c;找到以““Linux16”开头内容所在的行数”&#xff0c;在行的最后…

java-字符流和字节流(二)

java-字符流和字节流(二) 一、字节缓冲流 1.1字节缓冲流构造方法 字节缓冲流介绍 BufferOutputStream&#xff1a;该类实现缓冲输出流。 通过设置这样的输出流&#xff0c;应用程序可以向底层输出流写入字节&#xff0c;而不必为写入的每个字节导致底层系统的调用 BufferedIn…

chatgpt赋能python:Python动图如何优化SEO?

Python动图如何优化SEO&#xff1f; Python是一种高级编程语言&#xff0c;广泛应用于数据分析、人工智能和网站开发等领域。Python还支持创建动态图像&#xff0c;这些动态图像通常用于数据可视化、演示和教育目的。在本文中&#xff0c;我们将探讨如何使用Python创建动态图像…

chatgpt赋能python:Python加f之SEO的重要性

Python加f之SEO的重要性 随着互联网的不断发展和普及&#xff0c;越来越多的企业和个人纷纷进入到了网站建设&#xff0c;网络营销的大军之中。而SEO作为重要的一环&#xff0c;在各个领域内也变得愈加重要。而Python中的f字符串是近些年来引起广泛关注的一种新的字符串格式化…

chatgpt赋能python:Python动态内存分配:如何优化你的代码

Python动态内存分配&#xff1a;如何优化你的代码 在编写Python代码时&#xff0c;你可能已经注意到内存使用方面的一些问题。Python动态内存分配是一个重要的话题&#xff0c;它涉及到Python程序如何在运行时使用内存。本文将向您介绍Python动态内存分配的基本概念和如何优化…