当前位置: 欣欣网 > 码农

如何做到无感刷新token?

2024-03-14码农

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

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

解答:

突然闪退,一般都是由于你的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 classJwtUtil{
    // 有效期为
    publicstaticfinal Long JWT_TTL = 60 * 60 * 1000 * 24;// 60 * 60 * 1000 * 24 一个小时
    // 设置秘钥明文 --- 自己改就行
    publicstaticfinal String JWT_KEY = "qx";
    // 用于生成uuid,用来标识唯一
    publicstatic String getUUID(){
    String uuid = UUID.randomUUID().toString().replaceAll("-""");//token用UUID来代替
    return uuid;
    }
    /**
    id : 标识唯一
    subject : 我们想要加密存储的数据
    ttl : 我们想要设置的过期时间
    */

    // 生成token jwt加密 subject token中要存放的数据(json格式)
    publicstatic String createJWT(String subject){
    JwtBuilder builder = getJwtBuilder(subject, null, getUUID());// 设置过期时间
    return builder.compact();
    }
    // 生成token jwt加密
    publicstatic String createJWT(String subject, Long ttlMillis){
    JwtBuilder builder = getJwtBuilder(subject, ttlMillis, getUUID());// 设置过期时间
    return builder.compact();
    }
    // 创建token jwt加密
    publicstatic String createJWT(String id, String subject, Long ttlMillis){
    JwtBuilder builder = getJwtBuilder(subject, ttlMillis, id);// 设置过期时间
    return builder.compact();
    }
    privatestatic 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
    publicstatic SecretKey generalKey(){
    byte[] encodedKey = Base64.getDecoder().decode(JwtUtil.JWT_KEY);
    SecretKey key = new SecretKeySpec(encodedKey, 0, encodedKey.length, "AES");
    return key;
    }

    // jwt解密
    publicstatic Claims parseJWT(String jwt)throws Exception {
    SecretKey secretKey = generalKey();
    return Jwts.parser()
    .setSigningKey(secretKey)
    .parseClaimsJws(jwt)
    .getBody();
    }
    }








    写个单元测试,测试一下

    @Test
    voidtest()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);
    }
    // 解析令牌并获取过期时间
    publicstatic Date getTokenExpirationDate(String token){
    try {
    SecretKey secretKey = generalKey();
    Claims claims = Jwts.parser()
    .setSigningKey(secretKey)
    .parseClaimsJws(token)
    .getBody();
    return claims.getExpiration();
    catch (ExpiredJwtException | SignatureException e) {
    thrownew 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的过期时间延长。

    前端采用的是双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. 和后端的解决方案一样,前端做一个类似草稿箱的功能对表单等元素进行保存

    来源: juejin.cn/post/7316797749517631515

    >>

    END

    精品资料,超赞福利,免费领

    微信扫码/长按识别 添加【技术交流群

    群内每天分享精品学习资料

    最近开发整理了一个用于速刷面试题的小程序;其中收录了上千道常见面试题及答案(包含基础并发JVMMySQLRedisSpringSpringMVCSpringBootSpringCloud消息队列等多个类型),欢迎您的使用。

    👇👇

    👇点击"阅读原文",获取更多资料(持续更新中