AI翻译助手-JWT与redis无状态认证

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

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

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

JWT 负责「证明你是谁」

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

java 复制代码
@Component
public class JwtUtil {

    private static SecretKey KEY;

    public JwtUtil(@Value("${app.jwt.secret}") String secret) {
        byte[] keyBytes = secret.getBytes(StandardCharsets.UTF_8);
        if (keyBytes.length < 32) {
            throw new IllegalArgumentException("app.jwt.secret 长度必须至少 32 字节");
        }
        KEY = Keys.hmacShaKeyFor(keyBytes);
    }

    public static String generateToken(String subject, long ttlMillis, Map<String, Object> claims) {
        return Jwts.builder()
                .setSubject(subject)
                .addClaims(claims)
                .setIssuedAt(new Date())
                .setExpiration(new Date(System.currentTimeMillis() + ttlMillis))
                .signWith(KEY, SignatureAlgorithm.HS256)
                .compact();
    }

    public static Claims parseToken(String token) {
        return Jwts.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:

java 复制代码
public class AuthTokenInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        String token = extractToken(request);
        if (token == null) {
            unauthorized(response, "Missing token");
            return false;
        }
        // ① 验 JWT:签名 + 过期
        Claims claims = JwtUtil.parseToken(token);
        if (JwtUtil.isExpired(claims)) {
            unauthorized(response, "Token expired");
            return false;
        }
        // ② 查 Redis:token 是否还「活着」
        String userJson = redisTemplate.opsForValue().get("auth:token:" + token);
        if (userJson == null) {
            unauthorized(response, "Session expired");
            return false;
        }
        // ③ 还原用户,写入上下文
        User user = objectMapper.readValue(userJson.getBytes(), User.class);
        UserContext.set(user);
        return true;
    }
}

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

Token 怎么传

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

java 复制代码
private String extractToken(HttpServletRequest request) {
    String header = request.getHeader("Authorization");
    if (header != null && header.startsWith("Bearer ")) {
        return header.substring(7);
    }
    if (request.getCookies() != null) {
        for (var c : request.getCookies()) {
            if ("auth_token".equals(c.getName())) {
                return c.getValue();
            }
        }
    }
    return null;
}

一个最容易泄漏的坑

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

java 复制代码
UserContext.set(user);   // 存入 ThreadLocal

@Override
public void afterCompletion(...) {
    UserContext.clear();   // 必须清理
}

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

放行路径

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

java 复制代码
if (uri.startsWith("/static/") || uri.startsWith("/css/") || uri.startsWith("/js/") ||
        uri.startsWith("/images/") || uri.startsWith("/login") || uri.startsWith("/api/auth/")) {
    return true;
}

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

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


🌐 项目官网:智能翻译助手 AI Agent

相关推荐
胖头鱼的鱼缸(尹海文)2 小时前
胖头鱼的技术专栏-482 Oracle AI DB 23.26.4新特性一览(20261027)
数据库·人工智能·oracle
ss2732 小时前
PostgreSQL事务隔离级别与案例
数据库·postgresql
派小心.2 小时前
App数据统计平台怎么选?从SDK接入到日活留存看板的技术实现
数据库·人工智能·小程序
m0_639906942 小时前
MySQL 实现分布式锁:悲观锁、乐观锁、唯一索引锁
数据库·分布式·mysql
RuoyiOffice2 小时前
达梦 DM8 + openGauss:RuoYi Office 的 MySQL 数据转换与导入实践
数据库·spring boot·python·sql·信创
代码不停2 小时前
MyBatis操作数据库
数据库·mybatis
程序猿乐锅2 小时前
面向对象练习:图书借阅系统里的封装、抽象类与多态
开发语言·前端·数据库·后端·python·学习·1024程序员节
我不会起名字3222 小时前
Redis 从 0 到 1(十二):先删缓存还是先更新库?4 种方案的 Go 并发实测
redis·go·并发·延迟双删·缓存一致性
严同学正在努力2 小时前
PostgreSQL 高可用教程(流复制 + Patroni 生产级实战)
运维·数据库·ai·postgresql·架构