1. 背景与问题
在无状态的 HTTP 协议下,Web 应用通常使用 Session 或 Token 机制来维持用户的登录状态。在 Spring Boot 后端开发中,面临两个核心问题:
- 统一校验:如何在请求到达业务逻辑之前,统一拦截未登录请求,避免在每个 Controller 方法中重复编写校验代码?
- 上下文共享:在 Controller、Service 甚至 Dao 层中,如何优雅地获取当前登录用户的信息,而不需要层层传递
User对象参数?
这里介绍登录 + 拦截器 (Interceptor) + 线程本地变量 (ThreadLocal)”
2. 架构设计
该方案的流程如下:
- 登录接口:负责认证身份,并将用户信息存入 HttpSession。
- 拦截器 (
preHandle):在请求进入 Controller 之前拦截。校验 Session 中是否存在用户信息。- 若存在:将用户信息读取并存入当前线程的 ThreadLocal 中,放行。
- 若不存在:拦截请求,返回 401 状态码。
- 业务层 (Controller/Service):需要用户信息时,直接从 ThreadLocal 中获取,无需依赖 Session API。
- 拦截器 (
afterCompletion):请求结束,响应返回前,移除 ThreadLocal 中的用户信息,防止内存泄漏。
3. 代码实现步骤
3.1 封装 ThreadLocal 工具类 (UserHolder)
为了在同一线程内共享数据,我们需要封装一个基于 ThreadLocal 的工具类。它充当了线程内全局容器的角色。
1 | public class UserHolder { |
3.2 自定义登录拦截器 (LoginInterceptor)
拦截器实现 HandlerInterceptor 接口,负责“校验”和“上下文传递”。
1 | public class LoginInterceptor implements HandlerInterceptor { |
3.3 注册拦截器 (MvcConfig)
通过实现 WebMvcConfigurer 接口,将自定义拦截器注册到 Spring MVC 的拦截链中,并配置拦截路径。
1 | @Configuration |
3.4 业务层获取用户信息 (/user/me)
在 Controller 中,我们不再需要操作 HttpSession 或 HttpServletRequest,直接调用 UserHolder 即可。
1 | @GetMapping("/me") |
4. 组件关系
这套机制中,各组件的职责与关系如下:
4.1 拦截器 (Interceptor) 与 Session 的关系
- 关系:读取者与存储者。
- 解析:Session 是由 Tomcat 容器管理的服务器内存存储。拦截器作为请求处理的第一道关卡,负责从 Session 中读取状态。拦截器并不产生用户数据,它只是校验 Session 中是否已由登录接口写入了数据。
4.2 拦截器 (Interceptor) 与 ThreadLocal 的关系
- 关系:生产者与容器。
- 解析:这是本架构最精妙的地方。拦截器在
preHandle阶段充当“搬运工”,将 Session 中的数据(Web 作用域)复制到 ThreadLocal(线程作用域)。这使得后续的 Service 层代码可以完全脱离javax.servletAPI,实现了业务逻辑与 Web 容器的解耦。
4.3 Controller/Service 与 ThreadLocal 的关系
- 关系:消费者与容器。
- 解析:Controller 或 Service 层完全不需要知道拦截器的存在,也不需要知道数据是从 Session 来的还是 Token 来的。它们只需要信任
UserHolder,认为“只要代码执行到这里,UserHolder里一定有当前用户”,从而简化了代码逻辑。
5. 总结
实现这套登录校验拦截机制,本质上是为了解决两个工程化问题:
- 安全性(Secur11ity):通过拦截器实现统一的权限控制,防止未登录用户访问12受保护资源。
- 代码解耦(Decoupling):利用
ThreadLocal替代参数传递,使得用户信息可以在同一请求线程的任意位置被访问,极大地提高了代码的可维护性。
在后续的分布式演进中(如引入 Redis),我们只需要修改 登录接口(存 Redis) 和 拦截器(查 Redis) 的逻辑,而业务层获取用户信息的代码(UserHolder.getUser())完全不需要改动,这体现了良好的架构扩展性。