目录
一、单 token 认证
1、单 token 二次认证
2、单 token 二次认证 【自动续期】
3、单 token 三次认证【自动续期】
核心步骤:
设计思路:
二、双 token 认证
1、双token二次认证(长token续期)
2、双token三次认证 (自动续期)
核心机制:
设计思路:
三、有状态token的转变
1、如何“转变”为有状态token
2、代码
四、进阶用法
第一次认证:账号密码认证
第二次认证:Jwt 合法认证
优点:简单,资源消耗小
缺点:token时间设置长了,不安全,token时间设置短了,用户体验较差
第一次认证:账号密码认证
第二次认证:Jwt 合法认证
自动续期:每次权限认证成功后,都会返回一个新token
优点:相对简单,资源消耗相对较小,用户体验好
缺点:每次返回都需要生成一个新token,并且携带到响应体中,消耗资源。不安全
第一次认证:账号密码认证
第二次认证:Jwt 合法认证
第三次认证:token有效状态
自动续期:每次通过三次认证成功后,都会返回一个新token
将token按照某一规则进行转变,转变为一个有状态的token ,以此为redis中的key存入redis,当请求通过前两次认证后,将会将token转为有状态的token ,判断Redis中是否含有,校验成功,并在redis中删去此次请求中已使用过的 有状态令牌。 并生成新的token,在将token按照某一规则进行转变,转变为一个有状态的token,存入redis,返回资源时,将新token返回给前端。
以上操作的核心意义在token 令牌 赋予了只能一次性使用的特性,可以大大提升系统的安全性,弥补JWT token无法提前失效的弊端。即假如自动续签token的请求被黑客抓包,将其中的token重复使用,虽然能顺利通过第二种验证,但随即就会因为其所对应的“有状态token”已经在redis中被删而无法通过第三种验证。 同样由于一次性机制,即使令牌泄露并被黑客使用,也会被合法客户端及时发现——因为下一次自动续签时,合法客户端的令牌就会因为一次性机制失效了,继而要求用户重新登录,使得黑客手中的 token失效。也方便服务端根据登录、续签时的ip地址等记录判断该用户是否存在被盗号的风险,进一步进行账号冻结或提醒操作。
优点:用户体验好,大大提升系统的安全性
缺点:资源相对消耗较多
第一次认证:账号密码认证
第二次认证:Jwt 合法认证
refresh-token续期:401认证失败后,请求refresh-token续期请求
第一次认证:账号密码认证
第二次认证:Jwt 合法认证
第三次认证:有效token状态认证
自动续期:每次通过三次认证成功后,都会返回一个新长短token
首先在用户登陆阶段(流程概述的步骤1)做出改动,在生成了长短两个jwt token之外,还需生成一个有状态token,由长令牌按前面所说的规则“转变”而来。之后将长短令牌返回客户端,将有状态token作为键名存在redis中。留待后续的续签服务中使用。 之后在第三种验证时(流程概述的步骤5),只需将请求所携带的长令牌按照你设定的规则转变为为有状态token,再以此为键去redis中查询是否存在对应键即可。如果存在,则表示验证通过,其后的操作则与登录时一致,即生成长短令牌与有状态token,前俩令牌返回客户端,有状态令牌存redis,并在redis中删去此次请求中已使用过的 有状态令牌。
以上操作的核心意义在于给长令牌赋予了只能一次性使用的特性,可以大大提升系统的安全性,弥补JWT token无法提前失效的弊端。即假如自动续签token的请求被黑客抓包,将其中的长令牌重复使用,虽然能顺利通过第二种验证,但随即就会因为其所对应的“有状态token”已经在redis中被删而无法通过第三种验证。 同样由于一次性机制,即使长令牌泄露并被黑客使用,也会被合法客户端及时发现——因为下一次自动续签时,合法客户端的长令牌就会因为一次性机制失效了,继而要求用户重新登录,使得黑客手中的refresh token失效。也方便服务端根据登录、续签时的ip地址等记录判断该用户是否存在被盗号的风险,进一步进行账号冻结或提醒操作。 同时,纯JWT机制下,服务端系统原本无能为力的注销用户、拉黑名单等用户状态管理操作也得到了补全。注销时只需在redis中删去对应的有状态token,拉黑名单则在前者基础上进一步限制登陆即可,此时客户端发来的续签请求都会因为无法通过有状态token的验证而被拒。虽然客户端的短token还能在失效前继续可用,但已经利大于弊,采用JWT机制所失去的用户登录状态管理功能已经能够得到最大限度的挽回。
首先要介绍的是在第三种验证中提及的“转变”为有状态token。本质上是一个你自定义的规则,即要求你自定义一个方式,将格式为JWT的refresh_token转变为一个仅作为匹配一致性使用的、无意义的字符串,该转变只需要保证结果唯一,无需可逆。你可以对整个refresh_token进行一次md5计算(长度32),也可以直接把jwt token的签名部分截取使用(长度43)。开发阶段可以先使用后者,方便一眼看出对应关系。本文中把这个由长令牌转换来的字符串称为 “有状态token”,以区别于运用jwt规则验证的无状态token。
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
public class MatchTokenGenerator {
public static String generateMatchToken(String refreshToken) throws NoSuchAlgorithmException {
// 获取 SHA256 哈希算法实例
MessageDigest messageDigest = MessageDigest.getInstance("SHA-256");
// 将 refresh_token 转换为字节数组
byte[] refreshTokenBytes = refreshToken.getBytes();
// 对字节数组进行哈希计算
byte[] hashBytes = messageDigest.digest(refreshTokenBytes);
// 将哈希值转换为十六进制字符串
StringBuilder stringBuilder = new StringBuilder();
for (byte b : hashBytes) {
stringBuilder.append(String.format("%02x", b));
}
// 截取哈希值的前 16 个字符作为匹配字符串
String matchToken = stringBuilder.substring(0, 16);
return matchToken;
}
}
在上述代码中,我们首先获取 SHA256 哈希算法实例,然后将 refresh_token
转换为字节数组,并对其进行哈希计算。接着,我们将哈希值转换为十六进制字符串,并使用 StringBuilder
拼接字符串。最后,我们截取哈希值的前 16 个字符作为匹配字符串并返回。
注意,在使用哈希算法时,需要注意选择合适的算法和哈希值长度,以保证哈希冲突的概率尽可能小。另外,由于哈希算法的不可逆性,一旦生成了匹配字符串,就无法从中还原出原始的 refresh_token
。
同一用户多设备的情形下登陆状态的管理 基于上述设计,再稍稍扩展一下,也能做好同一用户多设备的情形下登陆状态的管理:比如限制用户同时在线的设备数目、限制用户在同一类设备上仅能同时在线一个等等。
拿其中最为复杂的“限制用户在同一类设备上仅能同时在线一个”举例,在redis中以用户id为键,再维护一套hash(子map)对照表。表的filed为设备类型,用户登陆时由客户端传来,如果对应filed不存在则直接保存,以设备类型为filed,以有状态token的字符串为值;如果对应filed已存在,则先根据其中存的值,找到对应有状态token的键并删除,再将“设备类型–token”的field–value对存入。此时同类型旧设备上所存的refresh_token即会因为对应有状态token被删而失效,需要用户重新登录。 所以“限制用户同时在线的设备数目”也更简单了,把上面的设计的hash结构改成list结构,用lua脚本确保list元素上限,超过了则连同有状态token的key一并移除,以此让最旧的设备上的refresh token失效。 并且,如果客户端需要加个“管理我的设备”功能,也能一并实现了。用户可以自行移除自己其他设备上的自动登录状态。