当前位置: 欣欣网 > 码农

如何做到无感刷新Token?

2024-04-25码农

推荐关注

扫码关注 后端架构师 」,选择 星标 公众号

重磅干货,第一时间送达!

责编:架构君 | 来源:https://juejin.cn/post/7316797749517631515

上一篇好文:

正文

大家好,我是后端架构师。

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

    你还有什么想要补充的吗?

    最后,再次推荐下我们的AI星

    为了跟上AI时代我干了一件事儿,我创建了一个知识星球社群:ChartGPT与副业。想带着大家一起探索 ChatGPT和新的AI时代

    有很多小伙伴搞不定ChatGPT账号,于是我们决定,凡是这三天之内加入ChatPGT的小伙伴,我们直接送一个正常可用的永久ChatGPT独立账户。

    不光是增长速度最快,我们的星球品质也绝对经得起考验,短短一个月时间,我们的课程团队发布了 8个专栏、18个副业项目

    简单说下这个星球能给大家提供什么:

    1、不断分享如何使用ChatGPT来完成各种任务,让你更高效地使用ChatGPT,以及副业思考、变现思路、创业案例、落地案例分享。

    2、分享ChatGPT的使用方法、最新资讯、商业价值。

    3、探讨未来关于ChatGPT的机遇,共同成长。

    4、帮助大家解决ChatGPT遇到的问题。

    5、 提供一整年的售后服务,一起搞副业

    星球福利:

    1、加入星球4天后,就送ChatGPT独立账号。

    2、邀请你加入ChatGPT会员交流群。

    3、赠送一份完整的ChatGPT手册和66个ChatGPT副业赚钱手册。

    其它福利还在筹划中... 不过,我给你大家保证,加入星球后,收获的价值会远远大于今天加入的门票费用 !

    本星球第一期原价 399 ,目前属于试运营,早鸟价 149 ,每超过50人涨价10元,星球马上要来一波大的涨价,如果你还在犹豫,可能最后就要以 更高价格加入了 。。

    早就是优势。 建议大家尽早以便宜的价格加入!

    欢迎有需要的同学试试,如果本文对您有帮助,也请帮忙点个 赞 + 在看 啦!❤️

    在 还有更多优质项目系统学习资源,欢迎分享给其他同学吧!

    PS:如果觉得我的分享不错,欢迎大家随手点赞、转发、在看。

    最后给读者整理了一份BAT大厂面试真题,需要的可扫码加微信备注:「面试」获取。

    版权申明:内容来源网络,版权归原创者所有。除非无法确认,我们都会标明作者及出处,如有侵权烦请告知,我们会立即删除并表示歉意。谢谢!

    END

    最近面试BAT,整理一份面试资料【Java面试BAT通关手册】,覆盖了Java核心技术、JVM、Java并发、SSM、微服务、数据库、数据结构等等。在这里,我为大家准备了一份2021年最新最全BAT等大厂Java面试经验总结。

    别找了,想获取史上最全的Java大厂面试题学习资料

    扫下方二维码回复面试就好了

    历史好文:

    扫码关注后端架构师」,选择星标公众号

    重磅干货,第一时间送达

    ,你在看吗?