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())完全不需要改动,这体现了良好的架构扩展性。

相关推荐
浮尘笔记5 小时前
Go语言临时对象池:sync.Pool的原理与使用
开发语言·后端·golang
梦梦代码精6 小时前
BuildingAI vs Dify vs 扣子:三大开源智能体平台架构风格对比
开发语言·前端·数据库·后端·架构·开源·推荐算法
REDcker7 小时前
RESTful API设计规范详解
服务器·后端·接口·api·restful·博客·后端开发
没有bug.的程序员9 小时前
Java 序列化:Serializable vs. Protobuf 的性能与兼容性深度对比
java·开发语言·后端·反射·序列化·serializable·protobuf
我爱娃哈哈10 小时前
SpringBoot + Spring Security + RBAC:企业级权限模型设计与动态菜单渲染实战
spring boot·后端·spring
小王不爱笑13212 小时前
SpringBoot 配置文件
java·spring boot·后端
想用offer打牌12 小时前
Spring AI vs Spring AI Alibaba
java·人工智能·后端·spring·系统架构
码农幻想梦13 小时前
实验五 spring入门及IOC实验
java·后端·spring
a程序小傲14 小时前
蚂蚁Java面试被问:向量数据库的相似度搜索和索引构建
开发语言·后端·python·架构·flask·fastapi
派大鑫wink15 小时前
【Day39】Spring 核心注解:@Component、@Autowired、@Configuration 等
java·后端·spring