通过最小项目落地 JWT 登录态,包含签发、拦截校验、ThreadLocal 传递与清理。
JWT + 拦截器 + ThreadLocal:我的登录态实现与踩坑记录
/ Update
4 mins
722 words
Loading views
JWT + 拦截器 + ThreadLocal 登录态实践:实现与踩坑记录h1
这个坑我踩过h2
我最早写接口时,图省事会让前端把 userId 直接放请求里,后面自己回头看都觉得风险太大。
后来做一个小项目时,我把登录态这块完整补了一遍:JWT 签发、拦截器校验、ThreadLocal 透传,再到请求结束清理。
这篇记录的就是那次“从能跑到靠谱一点”的改造过程。
这篇只解决四件事h2
实现以下闭环:
- 登录成功后签发 JWT
- 请求进入时用拦截器校验 Token
- 把用户信息放到
ThreadLocal - 业务代码随时获取当前登录用户
流程图(文字版)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
@Configurationpublic 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:方便跨层取当前用户
- 如何规避风险:请求结束清理 + 密钥外置 + 过期策略
评论