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);
    }

posted on 2021-03-31 13:45  precedeforetime  阅读(4273)  评论(0编辑  收藏  举报

导航