在开发过程中,如果在用户登录的前提下需要使用多线程,且在多线程中需要用到spring security中的用户认证信息时可以通过配置来让子线程可以通过以下几种方式获取到父线程中的用户信息.
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或者静态变量中,这个后续再详说,由此可以得到第一个最简单的方法来解决如题说的问题.
代码如下:
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也可以获取到用户信息.
? 除开上面的办法,还可以通过配置层面来解决这个问题,下面涉及到一点源码
? 简单的翻译下源码里面的解释,我觉得这个已经很清楚了
? SecurityContextHolder会给当前线程分配一个指定的SecurityContext.
? 这个类在内部维护了一个SecurityContextHolderStrategy实例,并在这个实例的基础上提供了一系列的静态方法,其目的是为了很方便的给JVM指定相应的strategy.由于类中的变量及方法都是static,所以这个类的相关设置是基于JVM层面的.
? 如果需要指定对应的strategy可以选择以下三种模式之一:
MODE_THREADLOCAL
MODE_INHERITABLETHREADLOCAL
MODE_GLOBAL
.或者自己实现org.springframework.security.core.context.SecurityContextHolderStrategy
接口,提供一个无参构造,然后将类的全限定名传入即可.? 有两种方式来指定strategy,第一种是通过设置JVM参数,第二种是在使用这个类之前调用
org.springframework.security.core.context.SecurityContextHolder#setStrategyName
方法,如果没有指定,则会默认使用MODE_THREADLOCAL
模式.
? 从以上可以看出,SecurityContextHolder类的核心在于SecurityContextHolderStrategy,而SecurityContextHolderStrategy就是用来保存SecurityContext的.SecurityContext中含有当前正在访问系统的用户的详细信息.默认情况下,SecurityContextHolder将使用ThreadLocal来保存SecurityContext,这也就意味着在处于同一线程中的方法中我们可以从ThreadLocal中获取到当前的SecurityContext.因为线程池的原因,如果我们每次在请求完成后都将ThreadLocal进行清除的话,那么我们把SecurityContext存放在ThreadLocal中还是比较安全的.这些工作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的值(父子线程引用同一个对象).
更改策略很简单,设置一个jvm参数即可-Dspring.security.strategy=MODE_INHERITABLETHREADLOCAL
即可,注意这个值不能设为环境变量,实测环境变量无法读取.下面是设置新的策略后debug结果:
配置参数后initialize方法会选择创建InheritableThreadLocalSecurityContextHolderStrategy.
项目正常启动并登录后,调用接口,可以看到父线程的inheritableThreadLocal中有了Authentication对象,如下:
进入子线程后,可以看到子线程inheritableThreadLocal也有Authentication对象
实际调用CurUserInfoUtil方法也能获取到用户信息,至此结束.
传入指定StrategyName即可,注意这个方法必须在项目启动(未使用SecurityContextHolder之前)的时候调用,否则会导致之前的用户Authentication信息获取不到.
/**
* 模拟登录,只是为了绕过日志审计,没有别的作用
*/
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);
}
spring security 如何在子线程中获取父线程中的用户认证信息
原文:https://www.cnblogs.com/precedeforetime/p/14601101.html