背景
因為我們的代碼中部分操作會有權限審計,在開發過程中,又經常會用到異步或者多線程,就會發現用戶明明登錄了,但是子線程卻讀不到用戶信息。
簡單看了下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_THREADLOCAL、MODE_INHERITABLETHREADLOCAL、MODE_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.SecurityContextHolder、org.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);
}