基于 Redis 的 JWT令牌失效方案

13 篇文章 0 订阅
10 篇文章 2 订阅

应用场景

当用户登录状态到登出状态时,对应的JWT的令牌需要设置为失效状态,这时可以使用基于 Redis 的黑名单方案来实现JWT令牌失效。

基于 Redis 的黑名单方案

当用户需要登出系统时,将用户携带的Token进行解析,解码出JWT令牌,取出对应的 UUID 过期时间 ,用过期的时间减去当前的时间,计算出这个Key的过期时间,再以这两个字段拼接作为 Key 并设置好过期时间存储到 Redis 中,如果有黑客拿窃取出来的JWT令牌进行登录,只要判断这个JWT令牌是否在黑名单就可以。

实现步骤

1.获得携带的Token解析并取出JWT令牌的代码

这段代码实现了对指定 JWT 的验证和使令牌失效的操作。

  1. 首先,通过调用 convertToken(headerToken) 方法将传入的头部令牌 headerToken 转换成实际的 JWT 字符串 token。
  2. 然后,使用 HMAC256 算法和预设的密钥 key 创建一个算法实例 algorithm。
  3. 接下来,使用算法实例 algorithm 构建一个 JWT 验证器 jwtVerifier。这个验证器将用于验证 JWT 的有效性。
  4. 在 try-catch 块中,首先通过调用 jwtVerifier.verify(token) 方法对 JWT 进行验证。如果验证成功,则返回一个 DecodedJWT 对象 verify,其中包含了 JWT 的解码信息,如令牌的唯一标识符(ID)和过期时间等。
  5. 接着,调用 deleteToken(verify.getId(), verify.getExpiresAt()) 方法来删除指定令牌,并将其加入到黑名单中进行失效处理。这里使用了 verify 对象中的 ID 和过期时间作为参数。
  6. 最后,如果在验证 JWT 过程中发生了 JWTVerificationException 异常,即 JWT 验证失败,则捕获该异常,并返回 false 表示令牌失效操作失败。
    /**
     * 让指定Jwt令牌失效
     * @param headerToken 请求头中携带的令牌
     * @return 是否操作成功
     */
    public boolean invalidateJwt(String headerToken){
        String token = this.convertToken(headerToken);
        Algorithm algorithm = Algorithm.HMAC256(key);
        JWTVerifier jwtVerifier = JWT.require(algorithm).build();
        try {
            DecodedJWT verify = jwtVerifier.verify(token);
            return deleteToken(verify.getId(), verify.getExpiresAt());
        } catch (JWTVerificationException e) {
            return false;
        }
    }

2.检查指定 UUID 的令牌是否为无效的(已加入黑名单)

这段代码用于检查指定 UUID 的令牌是否为无效的(已加入黑名单),通过判断 Redis 数据库中是否存在相应的键来决定令牌的有效性。如果键存在,则表示令牌已失效;如果键不存在,则表示令牌仍然有效。

    /**
     * 验证Token是否被列入Redis黑名单
     * @param uuid 令牌ID
     * @return 是否操作成功
     */
    private boolean isInvalidToken(String uuid){
        return Boolean.TRUE.equals(template.hasKey(Const.JWT_BLACK_LIST + uuid));
    }

3.将Token列入Redis黑名单中

这段代码实现了对指定令牌的删除和加入黑名单的操作,用于管理令牌的有效性和安全性。

  1. 首先,通过调用 isInvalidToken(uuid) 方法来检查指定的 UUID 是否为无效的令牌。如果 isInvalidToken 方法返回 true,则说明该令牌无效,此时直接返回 false,不执行后续操作。
  2. 获取当前时间 now,然后计算令牌的过期时间与当前时间的差值,并取最大值作为令牌的失效时间 expire。这里使用了 Math.max 方法来确保失效时间不会小于 0。
  3. 最后,通过 Redis 的 template 对象调用 opsForValue().set() 方法,将指定 UUID 的令牌加入到名为 Const.JWT_BLACK_LIST + uuid 的键中,并设置过期时间为 expire 毫秒。这样就将该令牌加入到了黑名单中,使其在一定时间后失效。
  4. 最终,方法返回 true 表示成功删除令牌并将其加入黑名单。
    /**
     * 将Token列入Redis黑名单中
     * @param uuid 令牌ID
     * @param time 过期时间
     * @return 是否操作成功
     */
    private boolean deleteToken(String uuid, Date time){
        if(this.isInvalidToken(uuid))
            return false;
        Date now = new Date();
        long expire = Math.max(time.getTime() - now.getTime(), 0);
        template.opsForValue().set(Const.JWT_BLACK_LIST + uuid, "", expire, TimeUnit.MILLISECONDS);
        return true;
    }
public final class Const {
    //JWT令牌
    public final static String JWT_BLACK_LIST = "jwt:blacklist:";
    public final static String JWT_FREQUENCY = "jwt:frequency:";
}

对应完整的代码如下:

@Component
public class JwtUtils {

    @Autowired
    private StringRedisTemplate template;

    @Value("${spring.security.jwt.key}")
    String key;

    @Value("${spring.security.jwt.expire}")
    int expire;

    /**
     * 让指定Jwt令牌失效
     * @param headerToken 请求头中携带的令牌
     * @return 是否操作成功
     */
    public boolean invalidateJwt(String headerToken){
        String token = this.convertToken(headerToken);
        Algorithm algorithm = Algorithm.HMAC256(key);
        JWTVerifier jwtVerifier = JWT.require(algorithm).build();
        try {
            DecodedJWT verify = jwtVerifier.verify(token);
            return deleteToken(verify.getId(), verify.getExpiresAt());
        } catch (JWTVerificationException e) {
            return false;
        }
    }

    /**
     * 将Token列入Redis黑名单中
     * @param uuid 令牌ID
     * @param time 过期时间
     * @return 是否操作成功
     */
    private boolean deleteToken(String uuid, Date time){
        if(this.isInvalidToken(uuid))
            return false;
        Date now = new Date();
        long expire = Math.max(time.getTime() - now.getTime(), 0);
        template.opsForValue().set(Const.JWT_BLACK_LIST + uuid, "", expire, TimeUnit.MILLISECONDS);
        return true;
    }

    /**
     * 验证Token是否被列入Redis黑名单
     * @param uuid 令牌ID
     * @return 是否操作成功
     */
    private boolean isInvalidToken(String uuid){
        return Boolean.TRUE.equals(template.hasKey(Const.JWT_BLACK_LIST + uuid));
    }

    public DecodedJWT resolveJwt(String headerToken) {
        String token = this.convertToken(headerToken);
        if (token == null) {
            return null;
        }
        Algorithm algorithm = Algorithm.HMAC256(key);
        JWTVerifier jwtVerifier = JWT.require(algorithm).build();
        try {
            DecodedJWT verify = jwtVerifier.verify(token);
            if(this.isInvalidToken(verify.getId())) return null;
            Date expireAt = verify.getExpiresAt();
            return new Date().after(expireAt) ? null : verify;
        } catch (JWTVerificationException e) {
            return null;
        }
    }

    public UserDetails toUser(DecodedJWT jwt) {
        Map<String, Claim> claims = jwt.getClaims();
        return User.withUsername(claims.get("name").asString())
                .password("********")
                .authorities(claims.get("authorities").asArray(String.class))
                .build();
    }

    public Integer toId(DecodedJWT jwt) {
        Map<String, Claim> claims = jwt.getClaims();
        return claims.get("id").asInt();
    }

    public String createJwt(UserDetails details, int id, String username) {
        Algorithm algorithm = Algorithm.HMAC256(key);
        Date expire = this.expireTime();
        return JWT.create()
                .withJWTId(UUID.randomUUID().toString())
                .withClaim("id", id)
                .withClaim("name", username)
                .withClaim("authorities", details.getAuthorities().stream().map(GrantedAuthority::getAuthority).toList())
                .withExpiresAt(expire)
                .withIssuedAt(new Date())
                .sign(algorithm);
    }

    public Date expireTime() {
        Calendar calendar = Calendar.getInstance();
        calendar.add(Calendar.HOUR, expire * 24);
        return calendar.getTime();
    }

    private String convertToken(String headerToken) {
        if(headerToken == null || !headerToken.startsWith("Bearer ")) {
            return null;
        }
        return headerToken.substring(7);
    }
}

  • 16
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值