如何做到无感刷新Token?

作者:じ☆ve宝贝

发布时间:2024-01-05T09:04:27

为什么需要无感刷新Token?

  • 「最近浏览到一个文章里面的提问,是这样的:」

    当我在系统页面上做业务操作的时候会出现突然闪退的情况,然后跳转到登录页面需要重新登录系统,系统使用了Redis做缓存来存储用户ID,和用户的token信息,这是什么问题呢?

  • 「解答:」

    突然闪退,一般都是由于你的token过期的问题,导致身份失效。

  • 「解决方案:」

    自动刷新token

    token续约

  • 「思路」

    如果Token即将过期,你在验证用户权限的同时,为用户生成一个新的Token并返回给客户端,客户端需要更新本地存储的Token,

    还可以做定时任务来刷新Token,可以不生成新的Token,在快过期的时候,直接给Token增加时间

自动刷新token

自动刷新token是属于后端的解决方案,由后端来检查一个Token的过期时间是否快要过期了,如果快要过期了,就往请求头中重新
放一个token,然后前端那边做拦截,拿到请求头里面的新的token,如果这个新的token和老的token不一致,直接将本地的token更换
接下来拿代码举例子
  • 先引入依赖

 
<dependency>
     <groupId>cn.hutool </groupId>
     <artifactId>hutool-all </artifactId>
     <version>5.5.1 </version>
</dependency>
<dependency>
     <groupId>com.alibaba </groupId>
     <artifactId>fastjson </artifactId>
     <version>1.2.33 </version>
</dependency>
<dependency>
     <groupId>io.jsonwebtoken </groupId>
     <artifactId>jjwt </artifactId>
     <version>0.9.1 </version>
</dependency>
  • 这是一个生成token的例子

 
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;

import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.util.Base64;
import java.util.Date;
import java.util.UUID;

public  class JwtUtil {
     // 有效期为
     public  static  final Long JWT_TTL =  60 *  60 *  1000 *  24; // 60 * 60 * 1000 * 24  一个小时
     // 设置秘钥明文 --- 自己改就行
     public  static  final String JWT_KEY =  "qx";
    // 用于生成uuid,用来标识唯一
     public static String getUUID(){
        String uuid = UUID.randomUUID().toString().replaceAll( "-"""); //token用UUID来代替
         return uuid;
    }

     /**
        id      : 标识唯一
        subject : 我们想要加密存储的数据
        ttl     : 我们想要设置的过期时间
     */


    // 生成token  jwt加密 subject token中要存放的数据(json格式)
     public static String createJWT(String subject) {
        JwtBuilder builder = getJwtBuilder(subject,  null, getUUID()); // 设置过期时间
         return builder.compact();
    }

     // 生成token  jwt加密
     public static String createJWT(String subject, Long ttlMillis) {
        JwtBuilder builder = getJwtBuilder(subject, ttlMillis, getUUID()); // 设置过期时间
         return builder.compact();
    }
  
     // 创建token jwt加密
     public static String createJWT(String id, String subject, Long ttlMillis) {
        JwtBuilder builder = getJwtBuilder(subject, ttlMillis, id); // 设置过期时间
         return builder.compact();
    }

     private static JwtBuilder getJwtBuilder(String subject, Long ttlMillis, String uuid) {
        SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
        SecretKey secretKey = generalKey();
         long nowMillis = System.currentTimeMillis();
        Date now =  new Date(nowMillis);
         if(ttlMillis== null){
            ttlMillis=JwtUtil.JWT_TTL;
        }
         long expMillis = nowMillis + ttlMillis;
        Date expDate =  new Date(expMillis);
         return Jwts.builder()
                .setId(uuid)               //唯一的ID
                .setSubject(subject)    // 主题  可以是JSON数据
                .setIssuer( "sg")      // 签发者
                .setIssuedAt(now)       // 签发时间
                .signWith(signatureAlgorithm, secretKey)  //使用HS256对称加密算法签名, 第二个参数为秘钥
                .setExpiration(expDate);
    }
    
     // 生成加密后的秘钥 secretKey
     public static SecretKey generalKey() {
         byte[] encodedKey = Base64.getDecoder().decode(JwtUtil.JWT_KEY);
        SecretKey key =  new SecretKeySpec(encodedKey,  0, encodedKey.length,  "AES");
         return key;
    }

     // jwt解密
     public static Claims parseJWT(String jwt) throws Exception {
        SecretKey secretKey = generalKey();
         return Jwts.parser()
                .setSigningKey(secretKey)
                .parseClaimsJws(jwt)
                .getBody();
    }
}
  • 写个单元测试,测试一下

 
@Test
void test() throws Exception {
    String token = JwtUtil.createJWT( "1735209949551763457");
    System.out.println( "Token: " + token);
    Date tokenExpirationDate = getTokenExpirationDate(token);
    System.out.println(tokenExpirationDate);
    System.out.println(tokenExpirationDate.toString());
     long exp = tokenExpirationDate.getTime();
     long cur = System.currentTimeMillis();
    System.out.println(exp);
    System.out.println(cur);
    System.out.println(exp - cur);
}
// 解析令牌并获取过期时间
public static Date getTokenExpirationDate(String token) {
     try {
        SecretKey secretKey = generalKey();
        Claims claims = Jwts.parser()
                .setSigningKey(secretKey)
                .parseClaimsJws(token)
                .getBody();
         return claims.getExpiration();
    }  catch (ExpiredJwtException | SignatureException e) {
         throw  new RuntimeException( "Invalid token", e);
    }
}
Token有点长,就不放全部了
可以看到我们的 exp 过期时间的毫秒数为 1703651262000
可以看到我们的 cur 当前时间的毫秒数为 1703564863035
我们将两者相减得到的值为 86398965ms,我们可以算一下一天的毫秒数是多少 1000 * 60 * 60 * 24 ms = 86400000ms
这样我们就能够拿到token的过期时间tokenExpirationDate了
我们就可以通过在校验token的时候,如果token校验通过了,此时我们拿到该token的过期时间,以(过期时间 - 当前时间)进行判断
如果说 (过期时间 - 当前时间) 小于约定的值,那么我们就重新根据token里面的信息,重新创建一个token,将新的token放到请求头中
返回给前端,前端去进行本地存储更新token

前端token续约

token的续约偏向于前端的解决方案,即由前端来进行token的过期时间的判断,首先前后端需要对接商量好一个token续约的接口,
当前端发现这个token快要过期的时候,向后端发送该token,然后后端将该token的过期时间延长。
「前端采用的是双Token的方式,access-token 和 refresh-token即 AT 和 RT」
「而对于纯后端的方式,就是只有access-token这一个token」
  • 「那么问题来了 AT 和 RT 到底有什么区别?为什么需要RT?」

    「在前端实现方案来说,RT是用来在AT即将过期的时候,用RT获取最新的token」

    我解释一下我的观点:

    AT的暴露机会更多,每个请求都要携带,所以设置的过期时间短一点,「减少劫持风险」

    RT只会暴露在auth服务中用来刷新at,设置的过期时间长一点,「增加便利性。」

    AT 和 RT 是为了网络传输安全,网络传输中,容易暴露 AT,因为 AT 时间短,暴露后风险系数才低

    「这种是标准的安全处理,其实已经无需探讨他的合理性,就好像 https 之于 http 一样」

疑问及思考

要是前端有一个表单页面,长时间不进行请求的发送,此时用户填写完表单了,再点击提交的时候,后端返回401了,怎么办?
也就是说,虽然你后端可以无感刷新Token,但是你后端无感刷新Token的前提是:前端得发请求,如果用户长时间不进行页面的交互,
即没有进行任何业务逻辑的跳转什么的,就单纯的往表单上面填东西,什么请求也没发的情况下,后端是无法感知Token过期的
「这种情况怎么解决?」
  • 对于纯后端的解决方案,我是这样想的

    让前端在表单填写内容的时候做处理,如果提交返回的是401,那么前端就需要获取表单存在本地存储 然后跳转登录页,登录成功后

    返回这个页面,然后从本地存储取出来再回显到表单上面。

  • 对于前端的解决方案,我是这样想的

    对于后端来说就是AT过期了,而对于前端来说就是AT和RT都过期了,怎么处理?

  1. 需要监听refresh token的过期时间,在接近过期的时候向后端发起请求来刷新refresh token 或者是定期刷新一下refresh token

  2. 和后端的解决方案一样,前端做一个类似草稿箱的功能对表单等元素进行保存

关于无状态的 JWT 到底应该怎样用?参见: https://t.zsxq.com/15xye5IWG