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 的工具类。它充当了线程内全局容器的角色。
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 中,我们不再需要操作 HttpSession 或 HttpServletRequest,直接调用 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.servletAPI,实现了业务逻辑与 Web 容器的解耦。
4.3 Controller/Service 与 ThreadLocal 的关系
- 关系 :消费者与容器。
- 解析 :Controller 或 Service 层完全不需要知道拦截器的存在,也不需要知道数据是从 Session 来的还是 Token 来的。它们只需要信任
UserHolder,认为"只要代码执行到这里,UserHolder里一定有当前用户",从而简化了代码逻辑。
5. 总结
实现这套登录校验拦截机制,本质上是为了解决两个工程化问题:
- 安全性(Secur11ity):通过拦截器实现统一的权限控制,防止未登录用户访问受保护资源。
- 代码解耦(Decoupling) :利用
ThreadLocal替代参数传递,使得用户信息可以在同一请求线程的任意位置被访问,极大地提高了代码的可维护性。
在后续的分布式演进中(如引入 Redis),我们只需要修改 登录接口(存 Redis) 和 拦截器(查 Redis) 的逻辑,而业务层获取用户信息的代码(UserHolder.getUser())完全不需要改动,这体现了良好的架构扩展性。