在 Spring Security 中,我就想從子線程獲取用戶登錄信息,怎么辦?


松哥原創的 Spring Boot 視頻教程已經殺青,感興趣的小伙伴戳這里-->Spring Boot+Vue+微人事視頻教程

大家知道在 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 的默認數據存儲策略:

修改完成后,再次啟動項目,就可以在子線程中獲取到登錄用戶數據了,至於原理,就是前面所講的。

5.小結

好啦,今天就和小伙伴們分享一下 SecurityContextHolder 中數據的存儲策略問題,感興趣的小伙伴可以自己試一試~

如果覺得有收獲,記得點個在看鼓勵下松哥哦~


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM