应用场景
当用户登录状态到登出状态时,对应的JWT的令牌需要设置为失效状态,这时可以使用基于 Redis 的黑名单方案来实现JWT令牌失效。
基于 Redis 的黑名单方案
当用户需要登出系统时,将用户携带的Token进行解析,解码出JWT令牌,取出对应的 UUID 和 过期时间 ,用过期的时间减去当前的时间,计算出这个Key的过期时间,再以这两个字段拼接作为 Key 并设置好过期时间存储到 Redis 中,如果有黑客拿窃取出来的JWT令牌进行登录,只要判断这个JWT令牌是否在黑名单就可以。
实现步骤
1.获得携带的Token解析并取出JWT令牌的代码
这段代码实现了对指定 JWT 的验证和使令牌失效的操作。
- 首先,通过调用 convertToken(headerToken) 方法将传入的头部令牌 headerToken 转换成实际的 JWT 字符串 token。
- 然后,使用 HMAC256 算法和预设的密钥 key 创建一个算法实例 algorithm。
- 接下来,使用算法实例 algorithm 构建一个 JWT 验证器 jwtVerifier。这个验证器将用于验证 JWT 的有效性。
- 在 try-catch 块中,首先通过调用 jwtVerifier.verify(token) 方法对 JWT 进行验证。如果验证成功,则返回一个 DecodedJWT 对象 verify,其中包含了 JWT 的解码信息,如令牌的唯一标识符(ID)和过期时间等。
- 接着,调用 deleteToken(verify.getId(), verify.getExpiresAt()) 方法来删除指定令牌,并将其加入到黑名单中进行失效处理。这里使用了 verify 对象中的 ID 和过期时间作为参数。
- 最后,如果在验证 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黑名单中
这段代码实现了对指定令牌的删除和加入黑名单的操作,用于管理令牌的有效性和安全性。
- 首先,通过调用 isInvalidToken(uuid) 方法来检查指定的 UUID 是否为无效的令牌。如果 isInvalidToken 方法返回 true,则说明该令牌无效,此时直接返回 false,不执行后续操作。
- 获取当前时间 now,然后计算令牌的过期时间与当前时间的差值,并取最大值作为令牌的失效时间 expire。这里使用了 Math.max 方法来确保失效时间不会小于 0。
- 最后,通过 Redis 的 template 对象调用 opsForValue().set() 方法,将指定 UUID 的令牌加入到名为 Const.JWT_BLACK_LIST + uuid 的键中,并设置过期时间为 expire 毫秒。这样就将该令牌加入到了黑名单中,使其在一定时间后失效。
- 最终,方法返回 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 {@Autowiredprivate 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);}
}