☰
AI翻译助手-JWT与redis无状态认证
2026/10/24 4:05:55 网站建设 项目流程

既用 JWT 又查 Redis,这个认证方案不矛盾

学认证的时候,都会遇到一个选择题:JWT 无状态,服务端不存 session,天然可扩展,但没法主动失效(踢人下线、改密码让旧 token 失效都做不到);Session 有状态,可主动失效,但要维护状态、分布式要共享。

我在这套系统里选了个「看似矛盾」的组合——既用 JWT,又用 Redis 存登录态。很多人会问:JWT 不是无状态的吗,你查 Redis 不是又变回有状态了?没错,这正是我想要的。这篇说清楚为什么。

JWT 负责「证明你是谁」

JWT 的核心作用是安全地传递身份信息,服务端不用存储就能验证:

@ComponentpublicclassJwtUtil{privatestaticSecretKeyKEY;publicJwtUtil(@Value("${app.jwt.secret}")Stringsecret){byte[]keyBytes=secret.getBytes(StandardCharsets.UTF_8);if(keyBytes.length<32){thrownewIllegalArgumentException("app.jwt.secret 长度必须至少 32 字节");}KEY=Keys.hmacShaKeyFor(keyBytes);}publicstaticStringgenerateToken(Stringsubject,longttlMillis,Map<String,Object>claims){returnJwts.builder().setSubject(subject).addClaims(claims).setIssuedAt(newDate()).setExpiration(newDate(System.currentTimeMillis()+ttlMillis)).signWith(KEY,SignatureAlgorithm.HS256).compact();}publicstaticClaimsparseToken(Stringtoken){returnJwts.parserBuilder().setSigningKey(KEY).build().parseClaimsJws(token).getBody();}}

两个安全细节:密钥从配置注入绝不硬编码,生产用JWT_SECRET环境变量覆盖;启动时校验密钥长度,HS256 要求至少 32 字节,不足直接启动失败,把配置错误挡在启动期而不是线上才炸。Token 有效期设 7 天,用户不用频繁登录。

Redis 负责「你还能不能用」

JWT 一旦签发,服务端就没法让它作废,除非等它过期。这在很多场景下不可接受:用户改密码希望旧 token 立即失效、管理员想踢掉某个用户、用户主动退出登录。所以我用 Redis 补上「主动失效」的能力。

登录时把 token 作为 key 存进 Redis:auth:token:{token},value 是用户信息 JSON,TTL 7 天和 JWT 一致。校验时不仅验 JWT,还要查 Redis:

publicclassAuthTokenInterceptorimplementsHandlerInterceptor{@OverridepublicbooleanpreHandle(HttpServletRequestrequest,HttpServletResponseresponse,Objecthandler){Stringtoken=extractToken(request);if(token==null){unauthorized(response,"Missing token");returnfalse;}// ① 验 JWT:签名 + 过期Claimsclaims=JwtUtil.parseToken(token);if(JwtUtil.isExpired(claims)){unauthorized(response,"Token expired");returnfalse;}// ② 查 Redis:token 是否还「活着」StringuserJson=redisTemplate.opsForValue().get("auth:token:"+token);if(userJson==null){unauthorized(response,"Session expired");returnfalse;}// ③ 还原用户,写入上下文Useruser=objectMapper.readValue(userJson.getBytes(),User.class);UserContext.set(user);returntrue;}}

登出的时候直接删掉 Redis 里的 key,token 立即失效——这是 JWT 单靠自身做不到的。

Token 怎么传

我支持两种方式,任选其一。Header 方式适合前后端分离、小程序、App 调 API;Cookie 方式适合浏览器端服务端渲染页面(我的 Thymeleaf 页面就靠这个):

privateStringextractToken(HttpServletRequestrequest){Stringheader=request.getHeader("Authorization");if(header!=null&&header.startsWith("Bearer ")){returnheader.substring(7);}if(request.getCookies()!=null){for(varc:request.getCookies()){if("auth_token".equals(c.getName())){returnc.getValue();}}}returnnull;}

一个最容易泄漏的坑

拦截器里用了UserContext(本质是ThreadLocal)保存当前用户。这里有个必须注意的点——ThreadLocal 用完必须清理:

UserContext.set(user);// 存入 ThreadLocal@OverridepublicvoidafterCompletion(...){UserContext.clear();// 必须清理}

因为 ThreadLocal 的生命周期和线程绑定,而 Spring 的请求处理线程是从线程池复用的。如果不清理,下一个请求复用同一个线程,就会读到上一个请求的用户信息——这在「串号」场景下是严重的安全事故,A 用户看到了 B 用户的数据。所以铁律是:ThreadLocal 用完必须 clear,且要在afterCompletion(无论成功失败都执行)里做。

放行路径

拦截器不是一刀切全拦,要精确放行不需要鉴权的路径:

if(uri.startsWith("/static/")||uri.startsWith("/css/")||uri.startsWith("/js/")||uri.startsWith("/images/")||uri.startsWith("/login")||uri.startsWith("/api/auth/")){returntrue;}

静态资源(css/js/图片)和登录、注册、验证码这些「还没登录就能用」的接口必须放行,否则用户连登录都登不进去。

「JWT + Redis」这套混合方案本质是各取所长:JWT 无状态地证明身份、可扩展;Redis 补上主动失效能力;ThreadLocal 在请求内传递当前用户。安全上记住三条就够了——密钥不硬编码、ThreadLocal 用完必 clear、放行路径精确。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询