写于:2019-01-04 19:52:37

# JWT 主动失效处理

之前的文章我们提到过,JWT一旦签发,就脱离了服务端的掌控,服务端只能通过设置JWT的过期时间“exp”声明,使得JWT在过期后被动失效。

但是在如下应用场景中,

  • 用户信息修改,重新签发 JWT “token”,针对旧有的 JWT 进行作废处理
  • 用户退出登录,需要主动让 JWT作废
  • 在JWT “token” 丢失泄露的情况下,我们需要主动让 JWT“token” 作废

等等。

JWT 使得分布式校验变得简单,其缺陷也很明显,就是对已经签发的 JWT 失去控制能力。

# 思考

1、如何对已签发的 JWT 能够进行有效的控制

需要将 JWT其存入相关介质中(内存,磁盘),而一般直接选择 Redis 等缓存数据库中。

2、那如果我们将 JWT 数据存入 缓存数据库中,那不就和 session 机制一样了

第一:我们 身份信息 存入到分布式缓存中,而正常的生产项目中一般有缓存机制,只需在架构体系原有的技术上,进行操作,工作量和操作简便性都由于 直接session 机制

第二:我们无需存储所有签发的JWT,只需要在相关业务逻辑触发的时候,针对对应的 JWT 的唯一标识 进行黑名单记录录入,相比较于session 机制,其数据量,和单数据体积都远小于 sesison机制。

# 设计与实现

# 设计

  • 签发的 JWT 需设定过期时间,且过期时间不宜太长
  • 签发的 JWT 都必须包含两个数据:用户身份标识,JWT 唯一标记(类似于商品的条形码)
  • 提供方法,当主动触发废弃 JWT 的时候,将 JWT 唯一标记,存入 Redis 缓存中,标记为 黑名单数据,同时设置过期时间,过期时间为 JWT 剩余的有效时长。
  • 提供方法,校验 JWT 有效性

# 实现

  • JWT 生成 token,校验token,获取token有效数据 工具类:JwtTokenUtils
/**
 * @Description jwt 工具类
 * @Author QGUOFENG
 */
public class JwtTokenUtils {

    /** 黑名单存入规则:前缀 + 自定义内容 **/
    public final static String JWT_BLACK_LIST_NAME = "JWT_BLACK_LIST_";

    /** 秘钥 **/
    private  String SECRET;
    /** token 过期时间: Calendar.SECOND */
    private  int calendarField;
    private  int calendarInterval;

    public JwtTokenUtils(String secret,int calendarField,int calendarInterval){
        this.SECRET = secret;
        this.calendarField = calendarField;
        this.calendarInterval = calendarInterval;
    }

    /**
     * JWT生成Token.<br/>
     * JWT构成: header, payload, signature
     * @param userId
     */
    public String createToken(Long userId){
        Date iatDate = new Date();
        // expire time
        Calendar nowTime = Calendar.getInstance();
        nowTime.add(calendarField, calendarInterval);
        Date expiresDate = nowTime.getTime();

        // header Map
        Map<String, Object> map = new HashMap<>();
        map.put("alg", "HS256");
        map.put("typ", "JWT");

        // build token
        // param backups {iss:Service, aud:APP}
        String token = JWT.create().withHeader(map)     // header
                .withClaim("iss", "Service") // payload
                .withClaim("aud", "APP")
                .withClaim("userId", null == userId ? null : userId.toString())
                .withClaim("mark", RandomCharUtils.timeMillionAddParam("_" + userId.toString())) // 标记,当jwt 失效时,将其加入黑名单中
                .withClaim("exp",expiresDate)
                .withIssuedAt(iatDate)                  // sign time
                .withExpiresAt(expiresDate)             // expire time
                .sign(Algorithm.HMAC256(SECRET));       // signature
        return token;
    }

    /**
     * <p>解密Token</p>
     * @param token
     * @return
     * @throws Exception
     */
    public  Map<String, Claim> verifyToken(String token) {
        DecodedJWT decodedJWT = decodedJWTInfo(token);
        if(null == decodedJWT){
            // 暂时返回空,可以进行其他的操作:例如 抛出自定义异常
            return null;
        }
        return decodedJWT.getClaims();
    }

    /**
     * <p> 根据 token 返回解密后的jwt 数据整体</p>
     * @param token
     * @return
     */
    public DecodedJWT decodedJWTInfo(String token){
        DecodedJWT jwt = null;
        try {
            JWTVerifier verifier = JWT.require(Algorithm.HMAC256(SECRET)).build();
            jwt = verifier.verify(token);
        } catch (Exception e) {
        }
        // 如果 token 已经过期,则 返回为空
        if(jwt == null){
            // 暂时返回空,可以进行其他的操作:例如 抛出自定义异常
            return null;
        }
        return jwt;
    }

    /**
     * <p>根据Token获取user_id</p>
     * @param token
     * @return user_id
     */
    public Long getUID(String token) {
        Claim userId = getClaim(token, "userId");
        return (null != userId) ? userId.as(Long.class) : 0L;
    }

    /**
     * <p>根据token获取mark</p>
     * @param token
     * @return
     */
    public String getMark(String token){
        Claim mark = getClaim(token, "mark");
        return (null != mark) ? mark.asString() : null;
    }

    /**
     * <p>根据 token 获取 过期时间 exp</p>
     * @param token
     * @return
     */
    public Date getExpireTime(String token){
        DecodedJWT decodedJWT = decodedJWTInfo(token);
        if(null == decodedJWT) { return null; }
        return decodedJWT.getExpiresAt();

    }

    /**
     * <p>根据 token 获取,该jwt 在几毫秒之后过期</p>
     * @param token
     * @return
     */
    public Long getExpireMillis(String token){
        Date expireTime = getExpireTime(token);
        return (null != expireTime) ? expireTime.getTime() - new Date().getTime() : 0L;
    }

    public Claim getClaim(String token,String name){
        Map<String, Claim> claims = verifyToken(token);
        if(null == claims || claims.isEmpty()){
            // 暂时返回空,可以进行其他的操作:例如 抛出自定义异常
            return null;
        }
        Claim claim = claims.get(name);
        if (null == claim || StringUtils.isEmpty(claim.asString())) {
            // 暂时返回空,可以进行其他的操作:例如 抛出自定义异常
            return null;
        }
        return claim;
    }
}

主要方法:createToken(Long userId)

生成的JWT 中存储的主要数据 ​ mark: JWT唯一标识 ​ exp :JWT过期时间 ​ userId:用户id

  • 服务接口定义:IJwtService 定义三个方法:
    • checkInBlackList(String authToken) 让 JWT 失效(存入黑名单中)
    • checkInBlackList(String authToken) 检测该 JWT 是否为黑名单数据
    • checkAuthTOkenValidity(String authToken) 检测 JWT 的有效性
/**
 * @Description jwt 服务接口定义
 * @Author QGUOFENG
 * @since V.1.0
 */
public interface IJwtService {

    /**
     * <p>进行 JWT 认证 token 的失效处理</p>
     * @param authToken
     * @return
     */
    boolean doFaildJwt(String authToken);

    /**
     * <p>认证 token 是否被强制过期</p>
     * @param authToken
     * @return false :非黑名单  true 黑名单
     */
    boolean checkInBlackList(String authToken);

    /**
     * <p>验证 jwt token的有效性:</p>
     *
     * @param authToken
     * @return false :无效 true 有效
     */
    boolean checkAuthTOkenValidity(String authToken);
}
  • redis 缓存实现:JwtRedisService
/**
 * @Description JWT redis 操作服务类
 * <p>第一版:提供 将 发放出去的 token 加入黑名单中</p>
 * @Author QGUOFENG
 */
@AllArgsConstructor
public class JwtRedisService implements IJwtService{

    /** 存入redis 缓存中 **/
    private RedisTemplate<String,String> redisTemplate;

    /** jwt token 操作工具类 **/
    private JwtTokenUtils jwtTokenUtils;


    @Override
    public boolean doFaildJwt(String authToken) {
        // 无效 token
        if(StringUtils.isBlank(authToken)){ return false;}

        Long expireMillis = jwtTokenUtils.getExpireMillis(authToken);
        // 已过期
        if(expireMillis.longValue() <= 0){ return false;}

        // token 标记,加入黑名单中
        String mark = jwtTokenUtils.getMark(authToken);
        if(StringUtils.isBlank(mark)){ return false; }
        redisTemplate.opsForValue().set(JwtTokenUtils.JWT_BLACK_LIST_NAME + mark, mark, expireMillis, TimeUnit. MILLISECONDS);
        return true;
    }

    @Override
    public boolean checkInBlackList(String authToken) {
        // 无效 token
        if(StringUtils.isBlank(authToken)){ return false;}

        Long expireMillis = jwtTokenUtils.getExpireMillis(authToken);
        // 已过期
        if(expireMillis.longValue() <= 0){ return false;}

        // token 标记,加入黑名单中
        String mark = jwtTokenUtils.getMark(authToken);
        if(StringUtils.isBlank(mark)){ return false; }

        String blackListCache = redisTemplate.opsForValue().get(JwtTokenUtils.JWT_BLACK_LIST_NAME + mark);
        if(StringUtils.isBlank(blackListCache)) { return false; }

        return true;
    }

    /**
     * <p>判定条件:
     *      *      1、是否空串
     *      *      2、是否过期
     *      *      3、无分组标记 :mark
     *      *      4、mark 是否在黑名单中
     *      * </p>
     * @param authToken
     * @return false :无效   true  :有效
     */
    @Override
    public boolean checkAuthTOkenValidity(String authToken) {
        // 无效 token
        if(StringUtils.isBlank(authToken)){ return false;}

        Long expireMillis = jwtTokenUtils.getExpireMillis(authToken);
        // 已过期
        if(expireMillis.longValue() <= 0){ return false;}

        // token 标记,加入黑名单中
        String mark = jwtTokenUtils.getMark(authToken);
        String blackListCache = redisTemplate.opsForValue().get(JwtTokenUtils.JWT_BLACK_LIST_NAME + mark);
        if(StringUtils.isNotBlank(blackListCache)){ return false; }

        // 用户身份标识是否存在
        Long uid = jwtTokenUtils.getUID(authToken);
        if(null == uid || uid.intValue() == 0){ return false; }
        return true;
    }
}

# 总结

这样的操作方式确实有悖于 JWT 设计的初衷,但这是一个解决使用场景的解决方案,技术和业务从来都不独立存在。技术服务与业务,业务造就了技术。

精彩内容推送,请关注公众号!
最近更新时间: 3/24/2020, 9:44:42 PM