axios实现无感知刷新token

项目背景

最近做了一个oa类的web项目,用户需要登录后才能正常访问页面。当用户登录成功后,后端接口会返给前端一个token,之后的每一次接口调用都需要携带token,服务端验证这个token来判断用户是否已经成功登录。当然token是由时效性的,当token过期时,我们可以重新登录来获取新的token,但这样做体验会很差,所以要求我们利用旧的token去换取新的token。也就是说我们要调用一个刷新token的接口,将之前的token传给服务端来换取最新的token,有了最新的token之后,我们再次调用之前因为token过期返回状态接口,这样就做到用户无感知刷新token了。

方案讨论

利用axios中的axios.interceptors.response.use()接口请求后拦截,当token过期时,我们通过调用刷新token方法获取最新的token之后,携带最新的token再重新进行一次请求。一般一个页面的展示数据都需要调用多个接口,如果token过期了,每个接口都调用一遍刷新token,这样势必会造成资源浪费,我们希望的是如果token过期了,我们只调用一次刷新token接口。这里我的解决方案是定义一个let isRefreshing = false;标签变量,标记当前是否正在刷新token的状态,如果正在刷新则不再调用刷新token的接口。这里还有一个难点需要解决,就是多个接口同时发起请求时,假设第一个接口先进入,接着执行刷新token接口,因为标签变量的原因,其它接口不会执行刷新token,我们需要将剩下的接口放入到一个队列中,当刷新token接口执行完毕之后,再依次执行被放入到队列中的接口,这里我们需要用到Promise将所有未执行的resolve放入到一个数组中,数组中每一个元素都处于pending状态,当刷新请求的接口返回来后,我们再调用resolve,逐个释放。

实现

这里我创建了一个localStorage.js文件,用于封装js的本地存储,我们要将从服务端获取的token存到浏览器缓存中,代码如下:

export default {
    //本地存储封装
    get(key){
        let data=localStorage.getItem(key);
        return JSON.parse(data)
    },
    set(key,val){
        let value=JSON.stringify(val);
        localStorage.setItem(key,value);
    }
}

创建了一个service.js文件,用于封装axios还有刷新token的实现,代码如下:

import axios from "axios"; //引入axios
import qs from "qs";//序列化post接口请求参数
import LocalStorage from "./localStorage.js"; //H5本地存储封装方法
let SECURITY_URL = "http://192.168.1.17:5505/security-amass/";//刷新token接口的base路径
axios.defaults.headers.post["Content-Type"] ="application/x-www-form-urlencoded;charset=UTF-8";
axios.defaults.headers.post["Access-Control-Allow-Origin"] = "*";
axios.defaults.withCredentials = false; // 携带cookie
// 创建一个axios实例
const instance = axios.create({
  timeout: 300000,
});
// 是否正在请求刷新token接口的标记
let isRefreshing = false;
// 请求队列
let requests = [];
//刷新token方法
function refreshToken(params) {
  return instance.get(SECURITY_URL + "oauth/token", params);
}
//请求结果拦截器
instance.interceptors.response.use(
  (response) => {
    // 接下来会在这里进行token过期的逻辑处理
    const config = response.config;
    let code = response.data.code;
    //这里的code值是跟后端约定好的, 40009代表token已经过期
    if (code == "40009") {
      if (!isRefreshing) {
        isRefreshing = true;
        let refresh_token = LocalStorage.get("refresh_token");
        //这里是我的项目中刷新token要传的参数,具体都传那些参数需要与后端开发确认
        let loginData = {
          grant_type: "refresh_token",
          client_id: "impawning",
          client_secret: "impawning",
          refresh_token,
        };
        refreshToken({ params: loginData })
          .then((res) => {
            let access_token = res.data.access_token;
            let refresh_token = res.data.refresh_token;
            LocalStorage.set("access_token", res.data.access_token);
            LocalStorage.set("refresh_token", res.data.refresh_token);
            config.headers["access_token"] = access_token;
            config.headers["refresh_token"] = refresh_token;
            requests.forEach((cb) => cb(access_token, refresh_token));
            requests = [];
            return instance(config);
          })
          .catch((err) => {
            window.location.href = "/";
          })
          .finally(() => {
            isRefreshing = false;
          });
      } else {
        // 正在刷新token,返回一个未执行resolve的promise
        return new Promise((resolve) => {
          // 将resolve放进队列,用一个函数形式来保存,等token刷新后直接执行
          requests.push((access_token, refresh_token) => {
            config.headers["access_token"] = access_token;
            config.headers["refresh_token"] = refresh_token;
            resolve(instance(config));
          });
        });
      }
    } else if (code == "40005" || code == "40003") {//这里代表token失效和token错误
      window.location.href = "/";
    } else {
      return response;
    }
  },
  (error) => {
    return Promise.reject(error);
  }
);

const api = {
  get(url, data) {
    instance.defaults.headers.common["access_token"] = LocalStorage.get(
      "access_token"
    );
    instance.defaults.headers.common["refresh_token"] = LocalStorage.get(
      "refresh_token"
    );
    return instance.get(url, { params: data });
  },
  post(url, data) {
    instance.defaults.headers.common["access_token"] = LocalStorage.get(
      "access_token"
    );
    instance.defaults.headers.common["refresh_token"] = LocalStorage.get(
      "refresh_token"
    );
    //post接口封装
    return instance.post(url, qs.stringify(data));
  },
};

export { api };

以上就是用axios实现无感知刷新token的示例,文章有疑问或错误的地方还请指出,感谢阅读。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容