ShaneD711's Blog.

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

2025/12/09
loading

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 的工具类。它充当了线程内全局容器的角色。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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 接口,负责“校验”和“上下文传递”。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
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 的拦截链中,并配置拦截路径。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@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 即可。

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

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

CATALOG
  1. 1. 1. 背景与问题
  2. 2. 2. 架构设计
  3. 3. 3. 代码实现步骤
    1. 3.1. 3.1 封装 ThreadLocal 工具类 (UserHolder)
    2. 3.2. 3.2 自定义登录拦截器 (LoginInterceptor)
    3. 3.3. 3.3 注册拦截器 (MvcConfig)
    4. 3.4. 3.4 业务层获取用户信息 (/user/me)
  4. 4. 4. 组件关系
    1. 4.1. 4.1 拦截器 (Interceptor) 与 Session 的关系
    2. 4.2. 4.2 拦截器 (Interceptor) 与 ThreadLocal 的关系
    3. 4.3. 4.3 Controller/Service 与 ThreadLocal 的关系
  5. 5. 5. 总结