首页
学习
活动
专区
圈层
工具
发布
30 篇文章
1
我又发现 Spring Security 中一个小秘密!
2
聊一个 GitHub 上开源的 RBAC 权限管理系统,很6!
3
Spring Security 中最流行的权限管理模型!
4
一个案例演示 Spring Security 中粒度超细的权限控制!
5
Spring Security 中如何细化权限粒度?
6
Spring Security 中的 hasRole 和 hasAuthority 有区别吗?
7
Spring Security 权限管理的投票器与表决机制
8
Spring Security 中如何让上级拥有下级的所有权限?
9
什么是计时攻击?Spring Boot 中该如何防御?
10
一个诡异的登录问题
11
为什么你使用的 Spring Security OAuth 过期了?松哥来和大家捋一捋!
12
深入理解 WebSecurityConfigurerAdapter【源码篇】
13
Spring Security 初始化流程梳理
14
花式玩 Spring Security ,这样的用户定义方式你可能没见过!
15
深入理解 AuthenticationManagerBuilder 【源码篇】
16
深入理解 SecurityConfigurer 【源码篇】
17
深入理解 FilterChainProxy【源码篇】
18
在 Spring Security 中,我就想从子线程获取用户登录信息,怎么办?
19
Spring Security 可以同时对接多个用户表?
20
Spring Security 竟然可以同时存在多个过滤器链?
21
一文搞定 Spring Security 异常处理机制!
22
Spring Security 配置中的 and 到底该怎么理解?
23
Spring Security 多种加密方案共存,老破旧系统整合利器!
24
神奇!自己 new 出来的对象一样也可以被 Spring 容器管理!
25
Spring Security 中的四种权限控制方式
26
Spring Boot+CAS 默认登录页面太丑了,怎么办?
27
Spring Boot+CAS 单点登录,如何对接数据库?
28
Spring Boot 实现单点登录的第三种方案!
29
松哥手把手教你入门 Spring Boot + CAS 单点登录
30
来一个简单的,微服务项目中如何管理依赖版本号?

在 Spring Security 中,我就想从子线程获取用户登录信息,怎么办?

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

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

代码语言:javascript
代码运行次数:0
复制
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.抛出问题

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

代码语言:javascript
代码运行次数:0
复制
@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();
}

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

代码语言:javascript
代码运行次数:0
复制
threadLocal.get() = javaboy
name+threadLocal.get() = Thread-121:null

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

代码语言:javascript
代码运行次数:0
复制
@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();
}

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

代码语言:javascript
代码运行次数:0
复制
threadLocal.get() = javaboy
name+threadLocal.get() = Thread-121:javaboy

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

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

2.ThreadLocal

我们先来分析一下 ThreadLocal。

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

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

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

代码语言:javascript
代码运行次数:0
复制
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 方法:

代码语言:javascript
代码运行次数:0
复制
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 进来的数据。

我们再来看下数据读取:

代码语言:javascript
代码运行次数:0
复制
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 的定义:

代码语言:javascript
代码运行次数:0
复制
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 方法:

代码语言:javascript
代码运行次数:0
复制
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

先来看一段代码:

代码语言:javascript
代码运行次数:0
复制
@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 的默认数据存储策略:

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

5.小结

好啦,今天就和小伙伴们分享一下 SecurityContextHolder 中数据的存储策略问题,感兴趣的小伙伴可以自己试一试~

下一篇
举报
领券