User-Profile-Image
hankin
  • 5
  • Java
  • Kotlin
  • Spring
  • Web
  • SQL
  • MegaData
  • More
  • Experience
  • Enamiĝu al vi
  • 分类
    • Zuul
    • Zookeeper
    • XML
    • WebSocket
    • Web Notes
    • Web
    • Vue
    • Thymeleaf
    • SQL Server
    • SQL Notes
    • SQL
    • SpringSecurity
    • SpringMVC
    • SpringJPA
    • SpringCloud
    • SpringBoot
    • Spring Notes
    • Spring
    • Servlet
    • Ribbon
    • Redis
    • RabbitMQ
    • Python
    • PostgreSQL
    • OAuth2
    • NOSQL
    • Netty
    • MySQL
    • MyBatis
    • More
    • MinIO
    • MegaData
    • Maven
    • LoadBalancer
    • Kotlin Notes
    • Kotlin
    • Kafka
    • jQuery
    • JavaScript
    • Java Notes
    • Java
    • Hystrix
    • Git
    • Gateway
    • Freemarker
    • Feign
    • Eureka
    • ElasticSearch
    • Docker
    • Consul
    • Ajax
    • ActiveMQ
  • 页面
    • 归档
    • 摘要
    • 杂图
    • 问题随笔
  • 友链
    • Spring Cloud Alibaba
    • Spring Cloud Alibaba - 指南
    • Spring Cloud
    • Nacos
    • Docker
    • ElasticSearch
    • Kotlin中文版
    • Kotlin易百
    • KotlinWeb3
    • KotlinNhooo
    • 前端开源搜索
    • Ktorm ORM
    • Ktorm-KSP
    • Ebean ORM
    • Maven
    • 江南一点雨
    • 江南国际站
    • 设计模式
    • 熊猫大佬
    • java学习
    • kotlin函数查询
    • Istio 服务网格
    • istio
    • Ktor 异步 Web 框架
    • PostGis
    • kuangstudy
    • 源码地图
    • it教程吧
    • Arthas-JVM调优
    • Electron
    • bugstack虫洞栈
    • github大佬宝典
    • Sa-Token
    • 前端技术胖
    • bennyhuo-Kt大佬
    • Rickiyang博客
    • 李大辉大佬博客
    • KOIN
    • SQLDelight
    • Exposed-Kt-ORM
    • Javalin—Web 框架
    • http4k—HTTP包
    • 爱威尔大佬
    • 小土豆
    • 小胖哥安全框架
    • 负雪明烛刷题
    • Kotlin-FP-Arrow
    • Lua参考手册
    • 美团文章
    • Java 全栈知识体系
    • 尼恩架构师学习
    • 现代 JavaScript 教程
    • GO相关文档
    • Go学习导航
    • GoCN社区
    • GO极客兔兔-案例
    • 讯飞星火GPT
    • Hollis博客
    • PostgreSQL德哥
    • 优质博客推荐
    • 半兽人大佬
    • 系列教程
    • PostgreSQL文章
    • 云原生资料库
    • 并发博客大佬
Help?

Please contact us on our email for need any support

Support
    首页   ›   Spring   ›   SpringSecurity   ›   正文
SpringSecurity

SpringSecurity—从子线程获取用户登录信息

2021-06-24 01:47:45
1090  0 0
参考目录 隐藏
1) 1.抛出问题
2) 2.ThreadLocal
3) 3.InheritableThreadLocal
4) 4.SpringSecurity

阅读完需:约 7 分钟

Spring Security 中想要获取登录用户信息,不能在子线程中获取,只能在当前线程中获取,其中一个重要的原因就是 SecurityContextHolder 默认将用户信息保存在 ThreadLocal 中。

但是实际上 SecurityContextHolder 一共定义了三种存储策略:

public class SecurityContextHolder {
 public static final String MODE_THREADLOCAL = "MODE_THREADLOCAL";
 public static final String MODE_INHERITABLETHREADLOCAL = "MODE_INHERITABLETHREADLOCAL";
 public static final String MODE_GLOBAL = "MODE_GLOBAL";
    ...
    ...
}

第二种存储策略 MODE_INHERITABLETHREADLOCAL 就支持在子线程中获取当前登录用户信息,而 MODE_INHERITABLETHREADLOCAL 的底层使用的就是 InheritableThreadLocal,那么 InheritableThreadLocal 和 ThreadLocal 有什么区别呢?为什么它就可以支持从子线程中获取数据呢?今天松哥就来和大家聊一聊这个话题。这个问题搞懂了,就理解了为什么在 Spring Security 中,只要我们稍加配置,就可以在子线程中获取到当前登录用户信息。

1.抛出问题

先来看一个大家可能都见过的例子:

@Test
void contextLoads() {
    ThreadLocal threadLocal = new ThreadLocal();
    threadLocal.set("javaboy");
    System.out.println("threadLocal.get() = " + threadLocal.get());
    new Thread(new Runnable() {
        @Override
        public void run() {
            String name = Thread.currentThread().getName();
            System.out.println("name+threadLocal.get() = " + name + ":" + threadLocal.get());
        }
    }).start();
}

这段代码的打印结果,相信大家都很清楚:

threadLocal.get() = javaboy
name+threadLocal.get() = Thread-121:null

数据在哪个线程存储,就要从哪个线程读取,子线程是读取不到的。如果我们把上面案例中的 ThreadLocal 修改为 InheritableThreadLocal,如下:

@Test
void contextLoads() {
    ThreadLocal threadLocal = new InheritableThreadLocal();
    threadLocal.set("javaboy");
    System.out.println("threadLocal.get() = " + threadLocal.get());
    new Thread(new Runnable() {
        @Override
        public void run() {
            String name = Thread.currentThread().getName();
            System.out.println("name+threadLocal.get() = " + name + ":" + threadLocal.get());
        }
    }).start();
}

此时的运行结果就会发生变化,如下:

threadLocal.get() = javaboy
name+threadLocal.get() = Thread-121:javaboy

可以看到,如果使用了 InheritableThreadLocal,即使在子线程中也能获取到父线程 ThreadLocal 中的数据。

那么这是怎么回事呢?我们一起来分析一下。

2.ThreadLocal

我们先来分析一下 ThreadLocal。

不看源码,仅从使用的角度来分析 ThreadLocal,大家会发现一个 ThreadLocal 只能存储一个对象,如果你需要存储多个对象,就需要多个 ThreadLocal 。

我们通过 ThreadLocal 源码来分析下。

当我们想要去调用 set 方法存储一个对象时,如下:

public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
}

大家可以看到,存储的时候会首先获取到一个 ThreadLocalMap 对象,获取的时候需要传入当前线程,看到这里大家可能就猜出来几分了,数据存储在一个类似于 Map 的 ThreadLocalMap 中,ThreadLocalMap 又和线程关联起来,怪不得每个线程只能获取到自己的数据。接下来我们来验证一下,继续看 getMap 方法:

ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}
void createMap(Thread t, T firstValue) {
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}

getMap 方法返回的是一个 threadLocals 变量,也就是说,数据是存在 threadLocals 中的。threadLocals 则就是一个 ThreadLocalMap。数据存入 ThreadLocalMap 实际上是保存在一个 Entry 数组中。在同一个线程中,一个 ThreadLocal 只能保存一个对象,如果需要保存多个对象,就需要多个 ThreadLocal,同一个线程中的多个 ThreadLocal 最终所保存的变量实际上在同一个 ThreadLocalMap 即同一个 Entry 数组之中。不同线程的 ThreadLocal 所保存的变量在不同的 Entry 数组中。Entry 数组中的 key 实际上就是 ThreadLocal 对象,value 则是 set 进来的数据。

我们再来看下数据读取:

public T get() {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    return setInitialValue();
}

首先根据当前线程获取到对应的 ThreadLocalMap,再传入当前对象获取到 Entry,然后将 Entry 对象中的 value 返回即可。有人可能会问,Entry 不是一个数组吗?为什么不传入一个数组下标去获取 Entry ,而是通过当前 ThreadLocal 对象去获取 Entry 呢?其实在 getEntry 方法中,就是根据当前对象计算出数组下标,然后将获取到的 Entry 返回。

3.InheritableThreadLocal

InheritableThreadLocal 实际上是 ThreadLocal 的子类,我们来看下 InheritableThreadLocal 的定义:

public class InheritableThreadLocal<T> extends ThreadLocal<T> {
    protected T childValue(T parentValue) {
        return parentValue;
    }
    ThreadLocalMap getMap(Thread t) {
       return t.inheritableThreadLocals;
    }
    void createMap(Thread t, T firstValue) {
        t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
    }
}

可以看到,主要就是重写了三个方法。getMap 方法的返回值变成了 inheritableThreadLocals 对象,createMap 方法中,构建出来的 inheritableThreadLocals 还依然是 ThreadLocalMap 的对象。和 ThreadLocal 相比,主要是保存数据的对象从 threadLocals 变为 inheritableThreadLocals。

这样的变化,对于前面的我们所说的 ThreadLocal 中的 get/set 并不影响,也就是 ThreadLocal 的特性依然不变。

变化发生在线程的初始化方法里,我们来看一下 Thread#init 方法:

private void init(ThreadGroup g, Runnable target, String name,
                  long stackSize, AccessControlContext acc,
                  boolean inheritThreadLocals) {
    ...
    ...
    if (inheritThreadLocals && parent.inheritableThreadLocals != null)
        this.inheritableThreadLocals =
            ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
    ...
    ...
}

可以看到,在创建子线程的时候,如果父线程存在 inheritableThreadLocals 变量且不为空,就调用 ThreadLocal.createInheritedMap 方法为子线程的 inheritableThreadLocals 变量赋值。ThreadLocal.createInheritedMap 方法所做的事情,其实就是将父线程的 inheritableThreadLocals 变量值赋值给子线程的 inheritableThreadLocals 变量。因此,在子线程中就可以访问到父线程 ThreadLocal 中的数据了。

需要注意的是,这种复制不是实时同步,有一个时间节点。在子线程创建的一瞬间,会将父线程 inheritableThreadLocals 变量的值赋值给子线程,一旦子线程创建成功了,如果用户再次去修改了父线程 inheritableThreadLocals 变量的值(即修改了父线程 ThreadLocal 中的数据),此时子线程是感知不到这个变化的。

经过上面的介绍相信大家就搞清楚 ThreadLocal 和 InheritableThreadLocal 的区别了。

4.SpringSecurity

先来看一段代码:

@GetMapping("/user")
public void userInfo() {
    Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
    String name = authentication.getName();
    Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
    System.out.println("name = " + name);
    System.out.println("authorities = " + authorities);
    new Thread(new Runnable() {
        @Override
        public void run() {
            Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
            String name = authentication.getName();
            Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
            String threadName = Thread.currentThread().getName();
            System.out.println(threadName + ":name = " + name);
            System.out.println(threadName + ":authorities = " + authorities);
        }
    }).start();
}

默认情况下,子线程中方法是无法获取到登录用户信息的。因为 SecurityContextHolder 中的数据保存在 ThreadLocal 中。

SecurityContextHolder 中通过 System.getProperty 来获取默认的数据存储策略,所以我们可以在项目启动时通过修改系统变量进而修改 SecurityContextHolder 的默认数据存储策略:

修改完成后,再次启动项目,就可以在子线程中获取到登录用户数据了,至于原理,就是前面所讲的。

如本文“对您有用”,欢迎随意打赏作者,让我们坚持创作!

0 打赏
Enamiĝu al vi
不要为明天忧虑.因为明天自有明天的忧虑.一天的难处一天当就够了。
543文章 68评论 294点赞 593913浏览

随机文章
SpringCloud—Zuul(一)
5年前
Kotlin-表达式—Lambda表达式(十四)
4年前
MySQL—理解索引 (1)
3年前
Spring—ApplicationEvent事件驱动机制
2年前
SpringMVC—MultipartResolver 解析(文件上传)
5年前
博客统计
  • 日志总数:543 篇
  • 评论数目:68 条
  • 建站日期:2020-03-06
  • 运行天数:1927 天
  • 标签总数:23 个
  • 最后更新:2024-12-20
Copyright © 2025 网站备案号: 浙ICP备20017730号 身体没有灵魂是死的,信心没有行为也是死的。
主页
页面
  • 归档
  • 摘要
  • 杂图
  • 问题随笔
博主
Enamiĝu al vi
Enamiĝu al vi 管理员
To be, or not to be
543 文章 68 评论 593913 浏览
测试
测试
看板娘
赞赏作者

请通过微信、支付宝 APP 扫一扫

感谢您对作者的支持!

 支付宝 微信支付