问题背景:用户权限莫名"穿越"?线上惊现越权访问!
最近在排查一个线上安全事件时,发现某用户的请求竟然处理成了其他用户的数据!经过代码审查,发现竟是ThreadLocal
使用不当埋下的"定时炸弹"💣。
场景复现 : 在用户登录鉴权模块中,我们将获取到的用户信息存入ThreadLocal
,后续业务代码通过UserContext.getCurrentUser()
获取。但在高并发场景下,部分请求竟然拿到了前一个用户的身份信息!
java
public class UserContext {
private static final ThreadLocal<User> currentUser = new ThreadLocal<>();
// 登录成功后设置用户信息
public static void setCurrentUser(User user) {
currentUser.set(user);
}
// 业务代码中获取用户信息
public static User getCurrentUser() {
return currentUser.get();
}
}
问题分析:线程池重用 + ThreadLocal未清理 = 数据"脏读"
根本原因在于Web服务器(如Tomcat)使用线程池处理请求:
- 线程处理完请求A后,
ThreadLocal
未清理 - 同一线程被复用来处理请求B
- 请求B未显式设置用户信息时,仍能读取到请求A的数据
⚠️ 这不是简单的内存泄漏,而是严重的安全漏洞!
解决方案:五重防御机制打造线程安全"结界"
- 强制清理:在Filter的finally块中清除数据
java
public class UserContextFilter implements Filter {
@Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
try {
// 鉴权逻辑...
UserContext.setCurrentUser(user);
chain.doFilter(req, res);
} finally {
UserContext.clear(); // 关键清理操作
}
}
}
- 防御性编程:封装移除方法,禁止外部直接操作
java
public class UserContext {
// 对外只提供remove,不暴露ThreadLocal实例
public static void clear() {
currentUser.remove();
}
private static ThreadLocal<User> currentUser = new ThreadLocal<>();
}
- 智能兜底:添加Null校验,避免NPE
java
public static User getCurrentUser() {
User user = currentUser.get();
if (user == null) {
throw new IllegalStateException("未初始化用户上下文!");
}
return user;
}
- 自动化测试:编写并发测试用例验证线程安全
java
@Test
void testConcurrentAccess() throws InterruptedException {
ExecutorService pool = Executors.newFixedThreadPool(10);
CountDownLatch latch = new CountDownLatch(100);
for (int i = 0; i < 100; i++) {
int userId = i;
pool.execute(() -> {
try {
UserContext.setCurrentUser(new User(userId));
// 模拟业务操作
Thread.sleep(10);
assertEquals(userId, UserContext.getCurrentUser().getId());
} finally {
UserContext.clear();
latch.countDown();
}
});
}
latch.await();
pool.shutdown();
}
- 监控预警:增加日志埋点,实时监控异常
java
public static User getCurrentUser() {
User user = currentUser.get();
if (user == null) {
log.error("检测到空用户上下文!请求路径:{}", getCurrentRequestPath());
// 接入监控平台报警
Monitor.report("USER_CONTEXT_NULL");
}
return user;
}
深度扩展:ThreadLocal的三大使用禁忌
- 避免存储大量对象:每个线程独立副本易导致内存膨胀
- 慎用InheritableThreadLocal:子线程继承可能引发数据污染
- 拒绝跨业务传递:异步场景需使用TransmittableThreadLocal等增强方案
总结
通过Filter强制清理 + 防御性编程 + 自动化测试的三重保障,我们彻底解决了用户数据"穿越"问题。技术细节决定系统安危,一个小小的ThreadLocal
使用不当,竟可能引发严重安全事故!
互动话题: 你在使用多线程时还遇到过哪些"坑"?欢迎在评论区分享讨论!💬