spring security 如何在子線程中獲取父線程中的用戶認證信息(更改安全策略)


背景

因為我們的代碼中部分操作會有權限審計,在開發過程中,又經常會用到異步或者多線程,就會發現用戶明明登錄了,但是子線程卻讀不到用戶信息。

簡單看了下spring security的源碼,發現有以下直接向ThreadLocal中添加Authentication對象更改spring security安全策略手動向ThreadLocal中添加權限校驗對象繞過檢驗三個解決辦法,其中前面兩個方法用起來較簡單。

以下代碼是我工作中使用到的一個靜態工具類,也用於下面的測試。

intellif.utils.CurUserInfoUtil#getUserInfo:
public static UserInfo getUserInfo() {
		Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
		//SecurityContextHolder.getContext().setAuthentication(authentication);
		if(null == authentication){
			return null;
		}
		return (UserInfo)authentication.getPrincipal();
	}

簡單的查看核心類SecurityContextHolder源碼,可以看到Authentication對象實質是保存當前線程的ThreadLocal中,這個是默認的實現方式,大部分情況已經夠用,另外還有可能存在InheritableThreadLocal或者靜態變量中,這個后續再詳說,由此可以得到第一個最簡單的方法,即復制ThreadLocal中的內容來解決如題說的問題。


直接向ThreadLocal中添加Authentication對象

代碼如下:

    private static final ForkJoinPool FORK_JOIN_POOL = new ForkJoinPool(Runtime.getRuntime().availableProcessors() + 4);        
    Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        List<String> list = new ArrayList<>();
        list.add("aaa");
        list.add("bbb");
        list.add("ccc");
        list.add("ddd");
        list.stream().map(t -> FORK_JOIN_POOL.submit(() -> threadAuthenticationTest(authentication,t))).
                collect(Collectors.toList()).forEach(FunctionUtil::waitTillThreadFinish);
    private void threadAuthenticationTest(Authentication authentication,String name) {
        SecurityContextHolder.getContext().setAuthentication(authentication);
        UserInfo userInfo = CurUserInfoUtil.getUserInfo();
        System.out.println("userInfo:" + userInfo.getLogin());
        System.out.println("name:" + name);
    }

直接從父線程中獲取到Authentication,然后通過傳參到子線程,最后子線程再放入SecurityContext中.

debug在子線程中可以看到ThreadLocal中的Authentication信息,通過CurUserinfoUtil也可以獲取到用戶信息.


更改spring security安全策略

關於這個方法,直接截取源碼中的一段描述:

spring security支持三種安全策略,MODE_THREADLOCALMODE_INHERITABLETHREADLOCALMODE_GLOBAL。如果沒有指定,則會默認使用MODE_THREADLOCAL策略。

有兩種方式來指定strategy,第一種是通過設置JVM參數 -Dspring.security.strategy=MODE_INHERITABLETHREADLOCAL

第二種是在項目啟動的時候調用org.springframework.security.core.context.SecurityContextHolder#setStrategyName方法。

三種策略的解釋如下,至於各個策略的具體實現原理,下面看源碼就知道了:

  • MODE_THREADLOCAL表示用戶信息只能由當前線程訪問。

  • MODE_INHERITABLETHREADLOCAL策略表示用戶信息可以由當前線程及其子線程訪問.

  • MODE_GLOBAL這個策略表示用戶信息沒有線程限制,全局都可以訪問,一般用於gui的開發中,這里可以忽略。


源碼

接下來看下源碼,源碼主要涉及到org.springframework.security.core.context.SecurityContextHolderorg.springframework.security.core.context.SecurityContextHolderStrategy兩個類,以SecurityContextHolder為入口,畢竟拿SecurityContext都是從它那里拿的。

SecurityContextHolder在內部維護了一個SecurityContextHolderStrategy實例,並在這個實例的基礎上提供了一系列的靜態方法。其功能只有兩個,一個是為了很方便的給JVM指定相應的strategy,一個是對外提供securityContext的讀取設置接口。

從以上可以看出,SecurityContextHolder類的核心在於SecurityContextHolderStrategy,而SecurityContextHolderStrategy接口的三個實現類就是通過不同類型的靜態常量contextHolder用來保存SecurityContext的,SecurityContext中含有當前正在訪問系統的用戶的詳細信息。

默認情況下,使用的org.springframework.security.core.context.ThreadLocalSecurityContextHolderStrategy實現類使用ThreadLocal來保存SecurityContext,這也就意味着我們只能在同一線程中從ThreadLocal獲取到當前的SecurityContext

補充一點,因為線程池中的線程會復用,如果每次使用之后線程中的用戶信息沒有清除,那么就有可能出現用戶信息錯亂的情況,好在這些工作Spring Security已經自動為我們做了,即在每一次request結束后都將清除當前線程的ThreadLocal。

​所謂的strategy設置本質上就是選擇SecurityContextHolderStrategy的不同實現類,spring security默認為我們提供了三種實現:

三者的區別就是存放SecurityContext對象的位置不同,顧名思義,默認的ThreadLocalSecurityContextHolderStrategy即是放在ThreadLocal中;

InheritableThreadLocalSecurityContextHolderStrategy的放在InheritableThreadLocal

GlobalSecurityContextHolderStrategy的通過源碼可以看出是其SecurityContext是一個靜態常量,即全局共享一個SecurityContext,這個具體也不是很清楚,據說用於C/S結構的客戶端。

ThreadLocal和InheritableThreadLocal的區別

InheritableThreadLocal繼承了ThreadLocal,與前者的區別是ThreadLocal只能由當前線程訪問,但是inheritableThreadLocal中的內容子線程也可以訪問,至於實現原理通過查看Thread源碼,可以看到在創建Thred對象(init方法)時,如果父線程的InheritableThreadLocal不為空的話,子線程會復制父線程的InheritableThreadLocal的值(父子線程引用同一個對象).


手動向ThreadLocal中添加權限校驗對象繞過檢驗

    /**
     * 模擬登錄,只是為了繞過日志審計,沒有別的作用
     */
    private static void login() {
        UserInfo userInfo = new UserInfo();
        userInfo.setLogin("landian");
        userInfo.setRoleTypeName(RoleTypes.SUPER_ADMIN.getName());
        userInfo.setRoleIds("1");
        userInfo.setPoliceStationId(1L);
        userInfo.setId(1);
        RoleInfo roleInfo = new RoleInfo();
        roleInfo.setId(1L);
        roleInfo.setCnName("超級管理員");
        roleInfo.setName("SUPER_ADMIN");
        roleInfo.setResIds("1,100,200,300,400,500,600,700,800");
        UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(userInfo, null, Collections.singletonList(roleInfo));
        OAuth2Authentication oAuth2Authentication = new OAuth2Authentication(null, usernamePasswordAuthenticationToken);
        Request request = new Request(null, null);
        InetSocketAddress inetSocketAddress = new InetSocketAddress("0.0.0.0", 65535);
        request.setRemoteAddr(inetSocketAddress);
        OAuth2AuthenticationDetails oAuth2AuthenticationDetails = new OAuth2AuthenticationDetails(request);
        oAuth2Authentication.setDetails(oAuth2AuthenticationDetails);
        SecurityContextHolder.getContext().setAuthentication(oAuth2Authentication);
    }


免責聲明!

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



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