需求: JWT用户无感知刷新,多个携带过期JWT的请求几乎同时访问不能出错,活跃用户JWT防持续盗用,单端在线。

Redis单实例实现:

每次Http请求带着JWT来访问,服务端都要按如下步骤验证/刷新令牌。

  1. 先校验JWT是否合法(只是过期,不影响校验通过) 或 数据1是否存在, 不满足任一条件,返回401,让用户重新登录。
  2. 判断数据2是否存在,存在就不需要继续验证了, 结束令牌校验,继续本次请求的业务逻辑
  3. 验证令牌是否过期,没过期结束令牌校验,继续本次请求的业务逻辑(此时,JWT合法性校验通过, 数据1存在,不存在数据2)。
  4. 令牌已过期,拿旧令牌(当前令牌),和新令牌(新生成的),调用lua脚本操作Redis刷新令牌
  5. 原子操作lua逻辑: 根据用户ID获取数据1的值,与旧令牌一致 把值换成新令牌 存入redis , 并且以旧令牌为KEY 保存数据2到redis, 返回 SUCCESS; 与旧令牌不一致(旧令牌没有刷新权限) 返回 FAIL
  6. 上一步 返回的结果是 SUCCESS,把新令牌放回http响应头中,并在http响应头中放入isnewToken标志着这是个新令牌

客户端统一拦截请求响应, 每次发送请求之前,把本地保存的令牌放到请求头里, 每次收到响应, 判断有没有isnewToken, 如果有,就把响应头中的令牌保存到本地。

redis数据结构 string

简称

key

value

有效时长(秒)

说明

数据1

用户ID

令牌

T1

令牌可以无感知刷新的时长,比如:7天60*60*24*7

数据2

令牌

用户ID

T2

平滑过渡时长,比如:30秒

/**
     * 登录/JWT过期后尝试刷新
     * @param key 用户ID
     * @param oldJwt 旧JWT
     * @param newJwt 新JWT
     * @return SUCCESS:成功, FAIL:失败
     */
    public String tryRefreshToken(String Key, String oldJwt, String newJwt) {
        String luaScript =
            "-- 根据key(用户ID)获取令牌 \n" +
            "local token = redis.call('get',KEYS[1])\n" +
            "-- 取到的token为空, 返回‘FAIL’ \n" +
            "if(not(tokens) or 0==#tokens) then return 'FAIL' end\n" +
            "-- 判断取到的token,是否与旧令牌一致 \n" +
            "-- 不一致,不能更新令牌 返回'FAIL' \n" +
            "if(token ~= ARGV[1]) then return 'FAIL' end\n" +
 "redis.call('set',KEYS[1],ARGV[2],'XX','EX',604800) \n" +
            "-- 保存平滑过渡令牌\n" +
            "redis.call('set',ARGV[1],KEYS[1],'NX','EX',30) \n" +
            "return 'SUCCESS'";

        return jedis.eval(luaScript, 1, key, oldJwt, newJwt));
    }