如何做到无感刷新Token?

👉 这是一个或许对你有用的社群

🐱 一对一交流/面试小册/简历优化/求职解惑,欢迎加入「芋道快速开发平台」知识星球。下面是星球提供的部分资料: 

dd267bb8d0ca66305e48bbd0925f4c1c.gif

👉这是一个或许对你有用的开源项目

国产 Star 破 10w+ 的开源项目,前端包括管理后台 + 微信小程序,后端支持单体和微服务架构。

功能涵盖 RBAC 权限、SaaS 多租户、数据权限、商城、支付、工作流、大屏报表、微信公众号等等功能:

  • Boot 仓库:https://gitee.com/zhijiantianya/ruoyi-vue-pro

  • Cloud 仓库:https://gitee.com/zhijiantianya/yudao-cloud

  • 视频教程:https://doc.iocoder.cn

【国内首批】支持 JDK 21 + SpringBoot 3.2.2、JDK 8 + Spring Boot 2.7.18 双版本 

来源:juejin.cn/post/
7316797749517631515

6f8435c21893e8d8b0c936ceca604288.jpeg


为什么需要无感刷新Token?

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

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

  • 「解答:」

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

  • 「解决方案:」

    自动刷新token

    token续约

  • 「思路」

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

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

基于 Spring Boot + MyBatis Plus + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能

  • 项目地址:https://github.com/YunaiV/ruoyi-vue-pro

  • 视频教程:https://doc.iocoder.cn/video/

自动刷新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有点长,就不放全部了

0ef3916227fa8bf474fae4a4fc07ca9d.jpeg

可以看到我们的 exp 过期时间的毫秒数为 1703651262000

可以看到我们的 cur 当前时间的毫秒数为 1703564863035

我们将两者相减得到的值为 86398965ms,我们可以算一下一天的毫秒数是多少 1000 * 60 * 60 * 24 ms = 86400000ms

这样我们就能够拿到token的过期时间tokenExpirationDate了

我们就可以通过在校验token的时候,如果token校验通过了,此时我们拿到该token的过期时间,以(过期时间 - 当前时间)进行判断

如果说 (过期时间 - 当前时间) 小于约定的值,那么我们就重新根据token里面的信息,重新创建一个token,将新的token放到请求头中

返回给前端,前端去进行本地存储更新token

基于 Spring Cloud Alibaba + Gateway + Nacos + RocketMQ + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能

  • 项目地址:https://github.com/YunaiV/yudao-cloud

  • 视频教程:https://doc.iocoder.cn/video/

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


欢迎加入我的知识星球,全面提升技术能力。

👉 加入方式,长按”或“扫描”下方二维码噢

a5ed2cb12e06afc68e7fd1e852b3a790.png

星球的内容包括:项目实战、面试招聘、源码解析、学习路线。

84c54b2a53ece304619e020ed4864f3e.png

3401ea856750f7401ea83a25aad46b55.png7bf1c3c05e16ddd4722c1f8b791739ef.pngbfa53b76c179db97302004db3b855bc0.png04303a65ba86b374dfcdaf2a08844951.png

文章有帮助的话,在看,转发吧。
谢谢支持哟 (*^__^*)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值