无感token刷新,我是怎么做的

Posted 恪愚

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了无感token刷新,我是怎么做的相关的知识,希望对你有一定的参考价值。

最近总是回想起大三时为了体(装)验(逼)提出要在学校新版系统中加入无感刷新token的功能。当时只是听到这个玩意甚至还没搞明白是咋回事,于是直到几个月后出来实习也没真正加上这个功能。

先说一个场景:我们都知道 JWT。假如说某系统将其token存留时间设置为5分钟。那么,如果说用户在这一时间快结束时恰好有其他事,回来以后再点“提交”,发现弹出“用户凭证已失效,请重新登录”。怎一个惨字了得!
当然,类似的场景还有许多,毫无疑问的是,如果过期时间设置的很短,用户就必须每隔一段时间重新登录,以获取新的凭证,这会极大挫伤用户的积极性。但是设置得太长,用户数据的安全性将大打折扣。

关于这一点,当时我与学弟们讨论了些许时间,最后得出以“埋点”为辅助,或监听用户行为,延长用户凭证存储时间 —— 可以发现,这里其实只是“巧妙”地解决了一个极限场景:若用户在过期时间前(很短时间时)又回来了。但实际上并没有解决上面说的问题。
最终还必须要借助双token,也就是标题说的“无感刷新token”。

解决思路也很简单:两个token存储(过期)时间不同。短token用来请求应用数据,长token用于获取新的短token。

后端设计逻辑

这里笔者必然用Koa实现。一共有三步:

  • 后端存有两个字段,分别保存长短token,并且每一段时间更新(?是否有其他方案可以用koa的ctx.state造一个全局的状态变量。但是就开发成本来说肯定是setInterval要低一些);
  • 为长token、短token约定单独的code值,便于前端排查、判断;
  • 请求头中新增两个字段携带表示长、短token(JWT思想),便于后端处理;

在router文件夹下的index.js文件中:

const router = require("koa-router")();
let accessToken = "s_token"; //短token
let refreshToken = "l_token"; //长token

// 30min刷新一次短token
setInterval(() => 
  accessToken = "s_tk" + Math.random();
, 300000);

// 12小时刷新一次长token
setInterval(() => 
  refreshToken = "l_tk" + Math.random();
, 7200000);

// 在登录接口后拿到token,存到前端
router.get("/login", async (ctx) => 
  ctx.body = 
    returncode: 0,
    accessToken,
    refreshToken,
  ;
);

// 获取短token
router.get("/refresh", async (ctx) => 
  //接收的请求头字段都是小写的
  let  pass  = ctx.headers;
  if (pass !== refreshToken) 
    ctx.body = 
      returncode: 108,
      info: "长token过期,重新登录",
    ;
   else 
    ctx.body = 
      returncode: 0,
      accessToken,
    ;
  
);

// 获取应用数据时后端校验一次token,这里如果过期了返回后前端不能跳登录页而是要重新拿token,续期
router.get("/getData", async (ctx) => 
  let  authorization  = ctx.headers;
  if (authorization !== accessToken) 
    ctx.body = 
      returncode: 104,
      info: "token过期",
    ;
   else 
    ctx.body = 
      code: 200,
      returncode: 0,
      data:  数据 ,
    ;
  
);

module.exports = router;

然后在主文件中引用:

const Koa = require('koa')
const app = new Koa();
const cors = require('koa2-cors');
const index = require('./router/index')

app.use(cors());
app.use(index.routes(),index.allowedMethods())

app.listen(8088,() => 
    console.log('server is listening on port 8088')
)

前端封装逻辑

新建config文件夹,处理请求事宜。
在config下的token_enum.js文件中,存放一些变量:

/* localStorage存储字段 */
export const ACCESS_TOKEN = "s_tk"; //短token
export const REFRESH_TOKEN = "l_tk"; //长token、
/* HTTP请求头字段 */
export const AUTH = "Authorization"; //存放短token
export const PASS = "PASS"; //存放长token

然后在code_map.js中存放前后端约定好的code:

// 在其它客户端被登录
export const CODE_LOGGED_OTHER = 106;
// 重新登陆
export const CODE_RELOGIN = 108;
// token过期
export const CODE_TOKEN_EXPIRED = 104;
//接口请求成功
export const CODE_SUCCESS = 0;

新建service文件夹,在其中的index.js文件中:

import axios from "axios";
import  refreshAccessToken, NoneTokenRequestList, clearAuthAndRedirect  from "./refresh";
import 
  CODE_LOGGED_OTHER,
  CODE_RELOGIN,
  CODE_TOKEN_EXPIRED,
  CODE_SUCCESS,
 from "../config/code_map.js";
import  ACCESS_TOKEN, AUTH  from "../config/token_enum.js";

const service = axios.create(
  baseURL: "//127.0.0.1:8088",
  timeout: 30000,
);

// 劫持请求,往request-header添加短token,用于后端处理
service.interceptors.request.use(
  (config) => 
    let  headers  = config;
    const s_tk = localStorage.getItem(ACCESS_TOKEN);
    s_tk &&
      Object.assign(headers, 
        [AUTH]: s_tk,
      );
    return config;
  ,
  (error) => 
    return Promise.reject(error);
  
);

// 劫持响应,
service.interceptors.response.use(
  (response) => 
    let  config, data  = response;
    //retry:第一次请求过期,接口调用refreshAccessToken,第二次重新请求,还是过期则reject出去
    let  retry  = config;
    return new Promise((resolve, reject) => 
      if (data["returncode"] !== CODE_SUCCESS) 
        if ([CODE_LOGGED_OTHER, CODE_RELOGIN].includes(data.returncode)) 
          clearAuthAndRedirect();
         else if (data["returncode"] === CODE_TOKEN_EXPIRED && !retry)  //当前是第一次调用发现token失效的情况
          config.retry = true;
          NoneTokenRequestList(() => resolve(service(config)));
          refreshAccessToken();
         else 
          return reject(data);
        
       else 
        resolve(data);
      
    );
  ,
  (error) => 
    return Promise.reject(error);
  
);

export default service;

在同级新建refresh.js文件:

import service from "./index.js";
import  ACCESS_TOKEN, REFRESH_TOKEN, PASS  from "../config/token_enum.js";

let subscribers = [];
let pending = false; //同时请求多个过期链接,保证只请求一次获取短token

export const NoneTokenRequestList = (request) => 
  subscribers.push(request);
;

export const retryRequest = () => 
  subscribers.forEach((request) => request());
  subscribers = [];
;

export const refreshAccessToken = async () => 
  if (!pending) 
    try 
      pending = true;
      const l_tk = localStorage.getItem(REFRESH_TOKEN);
      if (l_tk) 
        /* 利用长token重新获取短token */
        const  accessToken  = await service.get(
          "/refresh",
          Object.assign(,  headers:  [PASS]: l_tk  )
        );
        localStorage.setItem(ACCESS_TOKEN, accessToken);
        retryRequest();
      
      return;
     catch (e) 
      clearAuthAndRedirect();
      return;
     finally 
      pending = false;
    
  
;

/* 清除长短token,并定位到登录页(在项目中使用路由跳转) */
export const clearAuthAndRedirect = () =>
    localStorage.removeItem(ACCESS_TOKEN)
    window.location.href = '/login'

我觉得这样设计还很流批的一点是:它是上层封装,基本是不侵入业务代码的。


尾记

上海这一波极限操作搞得我学校也回不去,家也回不去,,,竟无语凝噎。想了想高中考完聚餐别人拍班照的时候我因为不胜酒力在旁边吐,现在又因为疫情回不去学校。合着我注定不能出现在班级照片中呗┗( ▔, ▔ )┛,一时竟悲从中来。

感谢学弟@*辉给我提供的一些想法,以及让我能虽然不在学校还能在学校系统中查看实际使用和与其他功能联动的效果,毕竟没有经过检验的想法注定只能是想法。我就有许多这种想法,但是马上就彻底跟学校分开了,不能白嫖学校资源了。一想到此,虽值五一佳节,更悲伤不已。

创作打卡挑战赛 赢取流量/现金/CSDN周边激励大奖

以上是关于无感token刷新,我是怎么做的的主要内容,如果未能解决你的问题,请参考以下文章

实现无感刷新 token 我是这样做的

实现无感刷新token我是这样做的

无感刷新token

前端如何实现token的无感刷新

记录-使用双token实现无感刷新,前后端详细代码

前端如何实现token的无感刷新