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

news2025/1/11 9:08:34

问题描述

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

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

public class BaseContext {

    public static ThreadLocal<Integer> 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获取用户权限。

所以按道理来说,最开始的那个前端的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。然后就奇妙的现象就这么出现了。

修复代码之后

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

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

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

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

ThreadLocal类

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

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

ThreadLocal<Integer> threadLocal = new ThreadLocal<>();

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

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

破案总结!

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

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

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

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1058792.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

javaWeb医疗管理系统

一、引言 1.1 系统背景 医疗行业一直是一个高度复杂和信息密集的领域。现代医院需要有效管理患者信息、医生信息、药物信息以及医疗记录等。本项目旨在通过开发一个JavaWeb医疗管理系统来满足这些需求。 1.2 目的和范围 这个系统的主要目标是帮助医院提高患者管理和医疗记录…

竞赛 机器视觉opencv答题卡识别系统

0 前言 &#x1f525; 优质竞赛项目系列&#xff0c;今天要分享的是 &#x1f6a9; 答题卡识别系统 - opencv python 图像识别 该项目较为新颖&#xff0c;适合作为竞赛课题方向&#xff0c;学长非常推荐&#xff01; &#x1f947;学长这里给一个题目综合评分(每项满分5分…

解决u盘在我的电脑中重复显示两个

删除注册表&#xff1a; [HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\Desktop\NameSpace\DelegateFolders\{F5FB2C77-0E2F-4A16-A381-3E560C68BC83}]

800*B. Long Long(贪心)

解析&#xff1a; 因为可以无限操作&#xff0c;所以最大值即为全部数字的绝对值&#xff0c;次数为连续负数区间的个数。 #include<bits/stdc.h> using namespace std; #define int long long const int N2e55; int t,n,a[N]; signed main(){scanf("%lld",&a…

力扣-367.有效的完全平方数

暴力 class Solution { public:bool isPerfectSquare(int num) {for(long i 1; i * i < num; i) {if(i * i num) return true;}return false;} };二分查找 class Solution { public:bool isPerfectSquare(int num) {int left 1, right num;while(left < right) {in…

Linux用户和权限

目录 1、root用户&#xff08;超级管理员&#xff09; su和exit命令 sudo命令 2、用户和用户组管理 用户组管理 用户管理 getent命令 3、查看权限控制信息 4、修改权限控制&#xff1a;chmod命令 5、修改权限控制&#xff1a;chown命令 1、root用户&#xff08;超级管…

Django之十三、添加用户之原始方法实现

修改urls.py path("user/add/", views.user_add),添加user_add.html {% extends layout.html %} {% block content %}<div class"container"><div class"panel panel-default"><div class"panel-heading"><h3 c…

AAD基础知识(identity/token/PRT)

简介 AAD(Azure Active Directory/Azure AD)是微软基于云身份验证和访问控制的解决方案&#xff0c;通过SSO登录其他o365应用(word/outlook/teams…) 微软在2023年7月把AAD重命名为Microsoft Entra ID&#xff0c;官网&#xff1a;https://www.microsoft.com/zh-cn/security/b…

LabVIEW开发带式谱感测技术

LabVIEW开发带式谱感测技术 如今&#xff0c;通过无线网络传输的数据量正在迅速增加&#xff0c;并导致频谱稀缺。超过数十亿的无线设备将被连接起来&#xff0c;并需要互联网接入。因此&#xff0c;无线电频谱管理方案的效率不足以授予对所有设备的访问权限。在频谱分配中&am…

深入学习JUC,深入了解Java线程中死锁与活锁问题,并理解其解决方法,笔记开记!!!

文章目录 死锁检查是否发生了死锁死锁的概念死锁产生的条件预防死锁解决死锁 活锁概念解决 ReentrantLock概念可重入可打断可超时可设置公平锁条件变量 死锁 检查是否发生了死锁 jstack通过 线程栈快照 定位线程中出现长时间停顿的原因, jconsole 图像界面 检查是否发生了死锁…

redis中list类型的操作

一、特点 Redis列表是简单的字符串列表&#xff0c;按照插入顺序排序。你可以添加一个元素到列表的头部&#xff08;左边&#xff09;或者尾部&#xff08;右边&#xff09;。一个列表最多可以包含 2^32 - 1 个元素 (超过40亿个元素)。 list其底层使用quicklist存储数据 qu…

mysql面试题10:MySQL中有哪几种锁?表级锁、行级锁、页面锁区别和联系?

该文章专注于面试,面试只要回答关键点即可,不需要对框架有非常深入的回答,如果你想应付面试,是足够了,抓住关键点 面试官:Mysql中有哪几种锁? 在MySQL中,主要有以下几种类型的锁: 共享锁(Shared Lock):也称为读锁。多个事务可以同时持有共享锁,可以读取但不能修…

想要精通算法和SQL的成长之路 - 验证二叉树的前序序列化

想要精通算法和SQL的成长之路 - 验证二叉树的前序序列化 前言一. 验证二叉树的前序序列化 前言 想要精通算法和SQL的成长之路 - 系列导航 一. 验证二叉树的前序序列化 原题链接 思路&#xff08;参考负雪明图&#xff09;&#xff1a; 首先我们看题目所给的字符串&#xff…

Ipython和Jupyter Notebook介绍

Ipython和Jupyter Notebook介绍 Python、IPython和Jupyter Notebook是三个不同但密切相关的工具。简而言之&#xff0c;Python是编程语言本身&#xff0c;IPython是对Python的增强版本&#xff0c;而Jupyter Notebook是一种在Web上进行交互式计算的环境&#xff0c;使用IPytho…

CSS学习小结

css的两种使用方式&#xff1a; ①内嵌样式表 ②导入外部样式表&#xff08;实际开发常用&#xff09;<link href"...." rel"stylesheet"/> 选择器&#xff1a; ①标签选择器&#xff1a;通过标签种类决定 ②类选择器&#xff1a;class"..…

websocket逆向【python实现http/https拦截】

python实现http拦截 前言:为什么要使用http拦截一、技术调研二、技术选择三、使用方法前言:为什么要使用http拦截 大多数爬虫玩家会直接选择API请求数据,但是有的网站需要解决扫码登录、Cookie校验、数字签名等,这种方法实现时间长,难度高。需求里面不需要高并发,有没有…

vertx的学习总结4

一、异步数据和事件流 1.为什么流是事件之上的一个有用的抽象&#xff1f; 2.什么是背压&#xff0c;为什么它是异步生产者和消费者的基础&#xff1f; 3.如何从流解析协议数据&#xff1f; 1. 答&#xff1a;因为它能够将连续的事件序列化并按照顺序进行处理。通过将事件…

ensp桥接电脑网卡

注意&#xff1a; 如果ensp云中没有你想要的网卡&#xff0c;请卸载电脑中的wincap&#xff0c;重新安装wincap即可。 wincap下载地址&#xff1a; 链接&#xff1a;https://pan.baidu.com/s/1JSXJSu4wKaiCKjGvY0mHKA?pwdh29v 提取码&#xff1a;h29v

【算法训练-数组 三】【结构特性】螺旋矩阵

废话不多说&#xff0c;喊一句号子鼓励自己&#xff1a;程序员永不失业&#xff0c;程序员走向架构&#xff01;本篇Blog的主题是螺旋矩阵&#xff0c;使用【二维数组】这个基本的数据结构来实现 螺旋矩阵【EASY】 二维数组的结构特性入手 题干 解题思路 根据题目示例 mat…

java Spring Boot 自动启动热部署 (别再改点东西就要重启啦)

上文 java Spring Boot 手动启动热部署 我们实现了一个手动热部署的代码 但其实很多人会觉得 这叫说明热开发呀 这么捞 写完还要手动去点一下 很不友好 其实我们开发人员肯定是希望重启这种事不需要自己手动去做 那么 当然可以 我们就让它自己去做 Build Project 这个操作 我们…