通过最小项目落地 JWT 登录态,包含签发、拦截校验、ThreadLocal 传递与清理。

JWT + 拦截器 + ThreadLocal:我的登录态实现与踩坑记录
/ Update
4 mins
722 words
Loading views

JWT + 拦截器 + ThreadLocal 登录态实践:实现与踩坑记录h1

这个坑我踩过h2

我最早写接口时,图省事会让前端把 userId 直接放请求里,后面自己回头看都觉得风险太大。

后来做一个小项目时,我把登录态这块完整补了一遍:JWT 签发、拦截器校验、ThreadLocal 透传,再到请求结束清理。

这篇记录的就是那次“从能跑到靠谱一点”的改造过程。

这篇只解决四件事h2

实现以下闭环:

  1. 登录成功后签发 JWT
  2. 请求进入时用拦截器校验 Token
  3. 把用户信息放到 ThreadLocal
  4. 业务代码随时获取当前登录用户

流程图(文字版)h2

用户登录 -> 服务端生成 JWT -> 客户端携带 Authorization
-> 拦截器解析 JWT -> 写入 UserContext(ThreadLocal)
-> Controller/Service 使用当前用户信息
-> 请求结束清理 ThreadLocal

第一步:定义 JWT 工具类h2

public class JwtUtils {
private static final String SECRET = "replace-with-your-secret";
public static String createToken(Long userId) {
Date now = new Date();
Date expire = new Date(now.getTime() + 24 * 60 * 60 * 1000L);
return Jwts.builder()
.setSubject(String.valueOf(userId))
.setIssuedAt(now)
.setExpiration(expire)
.signWith(SignatureAlgorithm.HS256, SECRET)
.compact();
}
public static Long parseUserId(String token) {
Claims claims = Jwts.parser()
.setSigningKey(SECRET)
.parseClaimsJws(token)
.getBody();
return Long.valueOf(claims.getSubject());
}
}

第二步:定义用户上下文h2

public class UserContext {
private static final ThreadLocal<Long> USER_HOLDER = new ThreadLocal<>();
public static void setUserId(Long userId) {
USER_HOLDER.set(userId);
}
public static Long getUserId() {
return USER_HOLDER.get();
}
public static void clear() {
USER_HOLDER.remove();
}
}

第三步:写登录拦截器h2

public class LoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) throws Exception {
String auth = request.getHeader("Authorization");
if (auth == null || !auth.startsWith("Bearer ")) {
response.setStatus(401);
return false;
}
String token = auth.substring(7);
try {
Long userId = JwtUtils.parseUserId(token);
UserContext.setUserId(userId);
return true;
} catch (Exception e) {
response.setStatus(401);
return false;
}
}
@Override
public void afterCompletion(HttpServletRequest request,
HttpServletResponse response,
Object handler,
Exception ex) {
UserContext.clear();
}
}

第四步:注册拦截器h2

@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LoginInterceptor())
.addPathPatterns("/**")
.excludePathPatterns("/auth/login", "/auth/register");
}
}

第五步:业务层取当前用户h2

@PostMapping("/post")
public Result<Long> createPost(@RequestBody PostDTO dto) {
Long userId = UserContext.getUserId();
return Result.ok(postService.createPost(userId, dto));
}

我当时踩到的 3 个坑h2

坑 1:忘了清理 ThreadLocalh3

如果不在 afterCompletion 里清理,线程复用时可能拿到上个请求的用户信息,问题很隐蔽。

坑 2:把密钥写死在代码里h3

开发期可以先写死,线上一定要放配置中心或环境变量。

坑 3:Token 过期后提示不清晰h3

建议区分“未登录”和“登录过期”,前端体验更好。

我自己怎么验收h2

  • 未携带 Token 是否返回 401
  • Token 错误是否返回 401
  • Token 过期是否返回 401
  • 登录后访问受保护接口是否成功
  • 并发请求下是否串用户

面试里我通常这样答h2

  • 为什么选 JWT:无状态、适合前后端分离
  • 为什么配合拦截器:统一校验入口
  • 为什么用 ThreadLocal:方便跨层取当前用户
  • 如何规避风险:请求结束清理 + 密钥外置 + 过期策略

评论