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_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);
}
posted on 2021-03-31 13:45 precedeforetime 阅读(4206) 评论(0) 编辑 收藏 举报