需求: JWT用户无感知刷新,多个携带过期JWT的请求几乎同时访问不能出错,活跃用户JWT防持续盗用,单端在线。
Redis单实例实现:
每次Http请求带着JWT来访问,服务端都要按如下步骤验证/刷新令牌。
- 先校验JWT是否合法(只是过期,不影响校验通过) 或 数据1是否存在, 不满足任一条件,返回401,让用户重新登录。
- 判断数据2是否存在,存在就不需要继续验证了,
结束令牌校验,继续本次请求的业务逻辑
。 - 验证令牌是否过期,
没过期
就结束令牌校验,继续本次请求的业务逻辑
(此时,JWT合法性校验通过, 数据1存在,不存在数据2)。 - 令牌已过期,拿
旧令牌
(当前令牌),和新令牌
(新生成的),调用lua脚本操作Redis刷新令牌 - 原子操作lua逻辑: 根据用户ID获取数据1的值,与
旧令牌
一致 把值换成新令牌
存入redis , 并且以旧令牌
为KEY 保存数据2到redis, 返回SUCCESS
; 与旧令牌
不一致(旧令牌没有刷新权限) 返回FAIL
- 上一步 返回的结果是
SUCCESS
,把新令牌
放回http响应头中,并在http响应头中放入isnewToken
标志着这是个新令牌
客户端统一拦截请求
和响应
, 每次发送请求之前,把本地保存的令牌放到请求头里, 每次收到响应, 判断有没有isnewToken
, 如果有,就把响应头中的令牌保存到本地。
redis数据结构 string
简称 | key | value | 有效时长(秒) | 说明 |
数据1 | 用户ID | 令牌 | T1 | 令牌可以无感知刷新的时长,比如: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));
}