Springboot——关于Springboot线程池时使用ThreadLocal 类的一个小小的漏洞

问题描述

前端的使用ajax发送了一个请求到后端

Springboot——关于Springboot线程池时使用ThreadLocal 类的一个小小的漏洞_第1张图片

后端自定义了一个线程上下文和实现了一个拦截器Interceptor

public class BaseContext {

    public static ThreadLocal threadLocal = new ThreadLocal<>();

    public static void setCurrentId(int id) {
        threadLocal.set(id);
    }

    public static int getCurrentId() {
        return threadLocal.get();
    }
    public  static Integer get(){
        return threadLocal.get();
    }

    public static void removeCurrentId() {
        threadLocal.remove();
    }

}
/**
 * jwt令牌校验的拦截器
 */
@Component
@Slf4j
public class JwtTokenInterceptor implements HandlerInterceptor {
    @Autowired
    private JwtProperties jwtProperties;
    /**
     * 校验jwt
     *  
     * @param request
     * @param response
     * @param handler
     * @return
     * @throws Exception
     */
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //判断当前拦截到的是Controller的方法还是其他资源
        if (!(handler instanceof HandlerMethod)) {
            //当前拦截到的不是动态方法,直接放行
            return true;
        }

//        String token = request.getHeader(jwtProperties.getTokenName());
        //1、从请求头中获取令牌
        String token = request.getHeader("Authorization");
        if (token != null && token.startsWith("Bearer ")) {
            token = token.substring(7); // 去除 "Bearer " 前缀
            // 现在你可以使用提取到的token进行处理
        }
            //2、校验令牌
            try {
                log.info("jwt校验:{}", token);
                Claims claims = JwtUtil.parseJWT(jwtProperties.getSecretKey(), token);
                int id = Integer.parseInt(claims.get("id").toString());
                log.info("当前用户id:{}", id);
                if(BaseContext.get()!=null)
                System.out.println("从线程池取出来的线程的已经存在的用户id:"+BaseContext.getCurrentId());
                BaseContext.setCurrentId(id); //存入线程上下文,每个用户请求独享一个线程,不会冲突
                System.out.println("刚刚更新的用户ID"+BaseContext.getCurrentId());
                //3、通过,放行
                return true;
            } catch (Exception ex) {
                //4、不通过,响应401状态码
                response.setStatus(401);
                return false;
            }
    }
}

然后拦截所有的非登录请求.

/**
 * 配置类,注册web层相关组件
 */
@Configuration
@Slf4j
public class WebMvcConfiguration extends WebMvcConfigurationSupport {

    @Autowired
    private JwtTokenInterceptor jwtTokenInterceptor;

    /**
     * 注册自定义拦截器
     * @param registry
     */
    protected void addInterceptors(InterceptorRegistry registry) {
        log.info("开始注册自定义拦截器...");
        registry.addInterceptor(jwtTokenInterceptor)
                .addPathPatterns("/hospital/**")
                .excludePathPatterns("/hospital/user/login");

    }
}

然后可以看见拦截路径都是前面要带一个"/hospital"的,并且每一个非login的请求都必须在请求头里面携带一个token,在拦截到的时候的要取出里面封装好的ID存入自定义的线程上下文。但是后端Controller层不小心写了一些不带/hospital前缀的请求路径,导致那些请求全都绕过了拦截器,没有在自定义的线程上下文里面存进去应存的id,但是我在服务层的所有方法都会在执行前先从线程上下文获取id,再通过id获取用户权限。

Springboot——关于Springboot线程池时使用ThreadLocal 类的一个小小的漏洞_第2张图片

所以按道理来说,最开始的那个前端的ajax请求即使在请求头里面携带了token也是没办法获取响应,因为没有被拦截存id,服务层获取不到自定义的线程上下文里面的id。

但是实际上却不是这样,那个前端请求有时候可以获得响应,有时候又不行。

 这就很奇怪了,明明那个请求雀氏没有经过拦截器,因为拦截器里面的日志语句一点反应也没有。

在那之前先了解一下springboot的线程池。

Springboot线程池

Springboot应用程序会为每一个用户请求分配一个独立的用户线程。

每个HTTP请求通常都会由一个独立的线程来处理,线程是请求的隔离单位。当一个请求到达时,Spring Boot会分配一个新的线程来处理该请求,这个线程会执行请求处理方法,并可以访问线程局部变量(如ThreadLocal中的变量)。

不同用户的请求会触发不同的线程,它们之间不会共享线程上下文,因此线程局部变量中的数据不会被其他用户的请求访问到。

但需要注意的是,在一些特殊情况下,例如使用线程池来处理请求时,线程可能会被重用。

重点Spring Boot应用程序通常使用线程池来管理和分配处理请求的线程。

在Spring中,Spring会在请求处理结束后自动清理线程上下文中的数据,但如果你手动创建线程或使用自定义线程池,需要自己负责线程上下文的清理。

所以我在上面自定义的线程上下文是不会被自动清理的。包括里面的数据。

解决问题

了解了Springboot应用程序中的线程池的机制之后可以知道,我在上面出现的那个时有时无的问题就是这么来的,首先一个别的正常的进入了拦截器的请求A被分配了一个用户线程A,然后在自定义线程上下文里面留下了一个用户id,结束请求之后,这个用户线程的自定义线程上下文没有被清理直接回到了线程池里面。

然后一个可以绕过拦截器的请求B进入之后被springboot从线程池里面拿出了刚刚回收的线程A的用户线程分配给了B,然后用户线程B在服务层成功从线程上下文里面取出了请求A存进去的用户A的id。然后就奇妙的现象就这么出现了。

Springboot——关于Springboot线程池时使用ThreadLocal 类的一个小小的漏洞_第3张图片

修复代码之后

Springboot——关于Springboot线程池时使用ThreadLocal 类的一个小小的漏洞_第4张图片

这一次发的请求绝对会被拦截器拦截。然后修改了拦截器里面的代码,在存入用户id之后先尝试从自定义线程上下文里面获取用户ID。

 按照上面的推论,我这里是有可能获得别的请求在线程留下的用户id的。

结果也果然如此,试了几次之后输出如下

在存入前先获取了一个无关的用户id. 

ThreadLocal类

ThreadLocal 是Java中的一个类,它用于创建线程局部变量。线程局部变量是一种特殊的变量,每个线程都有自己独立的副本,线程之间不共享这些变量的值。这使得线程可以在不干扰其他线程的情况下存储和访问自己的数据。

你提到的 new ThreadLocal<>(); 是创建一个新的 ThreadLocal 实例的方式,通常用于定义和管理线程局部变量。例如: 

ThreadLocal threadLocal = new ThreadLocal<>();

在上面的示例中,我们创建了一个 ThreadLocal 实例,该实例可以存储整数类型的线程局部变量。每个线程都可以通过这个 ThreadLocal 实例来访问自己的整数值,而不会影响其他线程的值。

ThreadLocal 主要用于在多线程环境下为每个线程存储和管理自己的数据,它在一些场景下非常有用,例如实现线程安全的数据库连接、用户身份验证、跟踪用户会话等。但要小心使用,确保在适当的时候清理 ThreadLocal 值,以避免潜在的内存泄漏问题。

破案总结!

在ThreadLocal类的时候要注意清理数据。

如果一个线程在线程池中被取出,然后在其执行过程中创建了 ThreadLocal 的实例并存入一些数据,然后将线程放回线程池,那么在下一次从线程池中取出同一个线程时,可能会看到之前存入的数据,因为 ThreadLocal 的值与线程相关,而不是与线程池相关。

这是因为 ThreadLocal 在每个线程内部维护了一个独立的副本(线程局部变量),这些副本在不同线程之间是隔离的。如果一个线程在 ThreadLocal 中存储了数据,那么只有同一个线程可以访问和修改这些数据。当线程被放回线程池时,线程池并不会主动清理线程内的 ThreadLocal 值,这意味着下一次取出同一个线程时,可能仍然可以看到之前存入的数据。

这种行为可以在某些情况下带来便利,但也需要谨慎使用,因为如果不正确管理 ThreadLocal 值,可能会导致内存泄漏或不一致的数据访问。在使用线程池时,特别要注意在线程结束后清理 ThreadLocal 值,以避免潜在的问题。通常,可以使用 ThreadLocal.remove() 方法来手动清理 ThreadLocal 值。在线程结束时或任务执行完成后调用 remove() 可以确保 ThreadLocal 值被清除,从而避免对下一次任务的影响。

你可能感兴趣的:(SpringBoot,spring,boot,后端,java)