Spring Boot 实战:基于拦截器与 ThreadLocal 的用户登录校验

1. 背景与问题

在无状态的 HTTP 协议下,Web 应用通常使用 SessionToken 机制来维持用户的登录状态。在 Spring Boot 后端开发中,面临两个核心问题:

  1. 统一校验:如何在请求到达业务逻辑之前,统一拦截未登录请求,避免在每个 Controller 方法中重复编写校验代码?
  2. 上下文共享 :在 Controller、Service 甚至 Dao 层中,如何优雅地获取当前登录用户的信息,而不需要层层传递 User 对象参数?

这里介绍登录 + 拦截器 (Interceptor) + 线程本地变量 (ThreadLocal)"

2. 架构设计

该方案的流程如下:

  1. 登录接口 :负责认证身份,并将用户信息存入 HttpSession
  2. 拦截器 (preHandle) :在请求进入 Controller 之前拦截。校验 Session 中是否存在用户信息。
    • 若存在:将用户信息读取并存入当前线程的 ThreadLocal 中,放行。
    • 若不存在:拦截请求,返回 401 状态码。
  3. 业务层 (Controller/Service) :需要用户信息时,直接从 ThreadLocal 中获取,无需依赖 Session API。
  4. 拦截器 (afterCompletion) :请求结束,响应返回前,移除 ThreadLocal 中的用户信息,防止内存泄漏。

3. 代码实现步骤

3.1 封装 ThreadLocal 工具类 (UserHolder)

为了在同一线程内共享数据,我们需要封装一个基于 ThreadLocal 的工具类。它充当了线程内全局容器的角色。

csharp 复制代码
public class UserHolder {
    // 使用 ThreadLocal 保存用户信息,UserDTO 为脱敏后的用户对象
    private static final ThreadLocal<UserDTO> tl = new ThreadLocal<>();

    // 保存用户
    public static void saveUser(UserDTO user){
        tl.set(user);
    }

    // 获取用户
    public static UserDTO getUser(){
        return tl.get();
    }

    // 移除用户(防止内存泄漏的关键)
    public static void removeUser(){
        tl.remove();
    }
}

3.2 自定义登录拦截器 (LoginInterceptor)

拦截器实现 HandlerInterceptor 接口,负责"校验"和"上下文传递"。

java 复制代码
public class LoginInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 1. 获取 Session
        HttpSession session = request.getSession();
        
        // 2. 获取 Session 中的用户
        Object user = session.getAttribute("user");
        
        // 3. 校验用户是否存在
        if (user == null) {
            // 4. 不存在,拦截,设置状态码 401 (未授权)
            response.setStatus(401);
            return false;
        }
        
        // 5. 存在,将用户信息保存到 ThreadLocal
        // 这一步实现了从 Web 层 (Session) 到 业务层 (ThreadLocal) 的状态转移
        UserHolder.saveUser((UserDTO) user);
        
        // 6. 放行
        return true;
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        // 7. 必须移除用户,避免线程池复用导致的"数据串号"和内存泄漏
        UserHolder.removeUser();
    }
}

3.3 注册拦截器 (MvcConfig)

通过实现 WebMvcConfigurer 接口,将自定义拦截器注册到 Spring MVC 的拦截链中,并配置拦截路径。

typescript 复制代码
@Configuration
public class MvcConfig implements WebMvcConfigurer {

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new LoginInterceptor())
                .excludePathPatterns(
                        "/user/code",   // 发送验证码
                        "/user/login",  // 登录接口
                        "/blog/hot",    // 热门博客
                        "/shop/**",     // 店铺详情
                        "/shop-type/**" // 店铺类型
                ).order(1);
    }
}

3.4 业务层获取用户信息 (/user/me)

在 Controller 中,我们不再需要操作 HttpSessionHttpServletRequest,直接调用 UserHolder 即可。

java 复制代码
@GetMapping("/me")
public Result me(){
    // 直接从 ThreadLocal 获取当前请求关联的用户信息
    UserDTO user = UserHolder.getUser();
    return Result.ok(user);
}

4. 组件关系

这套机制中,各组件的职责与关系如下:

4.1 拦截器 (Interceptor) 与 Session 的关系

  • 关系读取者与存储者
  • 解析:Session 是由 Tomcat 容器管理的服务器内存存储。拦截器作为请求处理的第一道关卡,负责从 Session 中读取状态。拦截器并不产生用户数据,它只是校验 Session 中是否已由登录接口写入了数据。

4.2 拦截器 (Interceptor) 与 ThreadLocal 的关系

  • 关系生产者与容器
  • 解析 :这是本架构最精妙的地方。拦截器在 preHandle 阶段充当"搬运工",将 Session 中的数据(Web 作用域)复制到 ThreadLocal(线程作用域)。这使得后续的 Service 层代码可以完全脱离 javax.servlet API,实现了业务逻辑与 Web 容器的解耦。

4.3 Controller/Service 与 ThreadLocal 的关系

  • 关系消费者与容器
  • 解析 :Controller 或 Service 层完全不需要知道拦截器的存在,也不需要知道数据是从 Session 来的还是 Token 来的。它们只需要信任 UserHolder,认为"只要代码执行到这里,UserHolder 里一定有当前用户",从而简化了代码逻辑。

5. 总结

实现这套登录校验拦截机制,本质上是为了解决两个工程化问题:

  1. 安全性(Secur11ity):通过拦截器实现统一的权限控制,防止未登录用户访问受保护资源。
  2. 代码解耦(Decoupling) :利用 ThreadLocal 替代参数传递,使得用户信息可以在同一请求线程的任意位置被访问,极大地提高了代码的可维护性。

在后续的分布式演进中(如引入 Redis),我们只需要修改 登录接口(存 Redis)拦截器(查 Redis) 的逻辑,而业务层获取用户信息的代码(UserHolder.getUser())完全不需要改动,这体现了良好的架构扩展性。

相关推荐
weixin_4250230015 小时前
Spring Boot 生成短链接
java·spring boot·后端
shark_chili15 小时前
浅谈CPU流水线的艺术
后端
秋饼15 小时前
【spring-framework 本地下载部署,以及环境搭建】
java·后端·spring
程序员泠零澪回家种桔子15 小时前
ReAct Agent 后端架构解析
后端·spring·设计模式·架构
程序员阿鹏16 小时前
RabbitMQ持久化到磁盘中有个节点断掉了怎么办?
java·开发语言·分布式·后端·spring·缓存·rabbitmq
superman超哥16 小时前
Rust 依赖管理与版本控制:Cargo 生态的精妙设计
开发语言·后端·rust·rust依赖管理·rust版本控制·cargo生态
『六哥』16 小时前
零基础搭建完成完整的前后端分离项目的准备工作
前端·后端·项目开发
不思念一个荒废的名字16 小时前
【黑马JavaWeb+AI知识梳理】Web后端开发08 - 总结
java·后端
冬奇Lab16 小时前
【Cursor进阶实战·01】Figma设计稿一键还原:Cursor + MCP让前端开发提速10倍
android·前端·后端·个人开发·figma
superman超哥16 小时前
Rust 泛型参数的使用:零成本抽象的类型级编程
开发语言·后端·rust·零成本抽象·rust泛型参数·类型级编程