axios使用异步方式无感刷新token,简单,太简单了

5,299 阅读6分钟

🍉 废话在前

写vue的伙伴们无感刷新token相信大家都不陌生了吧,刚好,最近自己的一个项目中就需要用到这个需求,因为之前没有弄过这个,研究了一个上午,终于还是把它拿下了,小小的一个token刷新😏。

OIP-C (1).jpg

下面接着分析一下踩到的坑以及解决思路

🍗 接着踩坑

我按照之前传统的方式在返回拦截器里面进行token刷新,正常的数据可以返回,但是这个时候会有比较麻烦的地方,就是请求的数据可以在拦截器里面得到,但是不能渲染到界面上(看到这里的时候我是懵的)。

看一下代码


service.interceptors.response.use(
  response => {
    const res = response.data
    //刷新token的时候,可以从这里拦截到新数据,但是没有显示在页面上
    console.log('拦截数据:',res)
    if (loadingInstance) {
      loadingInstance.close();
      NProgress.done();
    }
    switch (res.code) {
      case 200:
        return res
      case 401:
        router.push({
          path: "/login",
        })
        localStorage.clear();
        Notification.error({
          title: '令牌过期',
          message: '当前身份信息超过三天已失效,请您重新登录',
          duration: 0
        });
        break;
      default:
        Message({
          message: res.message || '请求错误',
          type: 'error',
          duration: 5 * 1000
        })
        break;
    }
  },
  error => {
    switch (error.response.status) {
      case 401:
        MessageBox.confirm('身份认证已过期,是否刷新本页继续浏览?', '提示', {
          confirmButtonText: '继续浏览',
          cancelButtonText: '退出登录',
          type: 'warning'
        }).then(() => {
          axios.post('/api/token/refresh/', {
            refresh: localStorage.getItem("retoken")
          }).then(response => {
            let res = response.data || {}
            if (res.code == 200) {
              let token = res.data.result;
              localStorage.setItem("token", token.access);
              localStorage.setItem("retoken", token.refresh);
              error.response.config.baseURL = '';
              error.response.config.headers['Authorization'] = 'Bearer  ' + localStorage.getItem("token")
              window.location.reload();
            } else {
              router.push({
                path: "/login",
              })
              localStorage.clear();
              Notification.error({
                title: '令牌过期',
                message: '当前身份信息超过三天已失效,请您重新登录',
                duration: 0
              });
            }
          }).catch(err => {
            router.push({
              path: "/login",
            })
            localStorage.clear();
            Notification.error({
              title: '认证失败',
              message: '信息认证失败,请重新登录',
              duration: 0
            });
          })
        }).catch(() => {
          router.push({
            path: "/login",

          })
          Message({
            message: '已退出',
            type: 'success',
          })
          localStorage.clear();
        });
        break;
      case 404:
        Notification.error({
          title: '404错误',
          message: '服务器请求错误,请联系管理员或稍后重试。错误状态码:404',
        });
        break
      default:
        Notification.error({
          title: '请求错误',
          message: '服务器请求错误,请联系管理员或稍后重试',
        });
        break;
    }
    if (loadingInstance) {
      loadingInstance.close();
      NProgress.done();
    }
    return Promise.reject(error);

  }
)

懵归懵,好在已经发现这个问题了,剩下的怎么解决呢?

当时想的是和后端配合,让后端直接发一个token过期的时间戳给我,我直接把这个时间戳放到localStrage里面,通过这个localStrage,直接在前端进行判断token的过期时间进行请求拦截,如果当前请求的时间大于了这个localStrage里面的时间,就说明token过期了,我这边就需要重新请求token了,而不要后端去进行token的验证。

信心满满的弄了一下,发现行不通,token过期的时候,后端直接一个错误401,前端就又回到解放前了。而且用时间戳的方式很容易出现bug,且在前端进行token时长的验证,很容易出现问题。因此,我还是觉得再研究一下上面那一段代码。

当然踩到坑不只是这个,还有百度和chatGPT,真的,看了一下没有找到一个可行的,总结一下主要有以下几种

  1. 状态管理vuex
  2. 路由router
  3. 时间戳(和我刚刚那种方式差不多)
  4. 依赖注入inject
  5. 刷新界面(我最开始那种方式,但是刷新的时候会出现页面白屏,且用户如果在页面上有一些自己输入的数据也会被清空,用户体验感不好)

以上方式我先不管行不行,但是麻烦是肯定的,做前端讲究的就一个字:“懒” 不是,应该是 “高效快捷” 所以这些方式就pass掉了

只能想想为什么拦截器里面可以得到数据,为什么页面位置得不到数据了

🥩 解决思路

下面是我的解决思路,有不对的地方还请看到的大神指出来一下😁

当用户发起请求的时候,因为刷新token的http状态码是401,这个时候axios的响应拦截器就直接进行错误捕获了,到了这里,因为数据已经返回了,但是因为是错误数据,页面得到的这个数据不可用且当前请求已经结束了,当然这里对状态码401是进行处理了的(应该获取token了)。

采用普通的获取方式来获取token,因为异步的原因,我们获取token的同时页面也在做刷新,token获取的同时,界面也刷新完毕了(但是是没有数据的,不做错误捕获会报错),因此我们在获取token完毕,且用新的token去获取数据时,拦截器里面会有数据,但是界面已经休息了,就不会把拦截器里面的新数据刷新到页面了。

因此这个地方需要对获取token的过程进行一下请求阻塞,把获取token的请求变成同步的。到这里就差不多了。直接把响应拦截器里面的error函数变成同步不就行了吗,async + await可以出来了。

以上是自己当时的想法,简单说来就是 页面刷新需要慢我获取token一步 ,通过这个方式也确实做到了无感刷新🤣

希望以上的能帮助到你,有什么好的思路也欢迎评论区指出。

🍓完整代码

这个是用了2.13.2版本的element-ui以及nprogress的一个axios代码模块,包含了一个下载文件的模块。

如果需要的话可以根据自己的需求来进行修改

import axios from 'axios'
import {
  Message,
  Loading,
  Notification,
} from 'element-ui'
import NProgress from 'nprogress' // progress bar
import 'nprogress/nprogress.css' // progress bar style
import router from '@/router/index'
const baseURL = '/api'
const service = axios.create({
  baseURL,
  timeout: 6000
})
NProgress.configure({
  showSpinner: false
}) // NProgress Configuration
let loadingInstance = undefined;
service.interceptors.request.use(
  config => {
    NProgress.start()
    loadingInstance = Loading.service({
      lock: true,
      text: '正在加载,请稍候...',
      spinner: 'el-icon-loading',
      background: 'rgba(0, 0, 0, 0.3)'
    })
    config.headers['Authorization'] = 'Bearer  ' + localStorage.getItem("token")
    return config;
  },
  error => {
    return Promise.reject(error)
  }
)

service.interceptors.response.use(
  response => {
    const res = response.data

    if (loadingInstance) {
      loadingInstance.close();
      NProgress.done();
    }
    switch (res.code) {
      case 200:
        return res
      case 401:
        router.push({
          path: "/login",
        })
        localStorage.clear();
        Notification.error({
          title: '认证失效',
          message: '当前身份信息超过三天已失效,请您重新登录',
          duration: 0
        });
        break;
      default:
        Message({
          message: res.message || '请求错误',
          type: 'error',
          duration: 5 * 1000
        })
        break;
    }
  },
  async error => {
    switch (error.response.status) {
      case 401:
        const err401Data = error.response.data || {}
        if (err401Data.code !== "token_not_valid") {
          router.push({
            path: "/login",
          })
          localStorage.clear();
          Notification.error({
            title: '认证失败',
            message: '身份信息认证失败,请您重新登录',
            duration: 0
          });
          return
        }
        try {
          const res = await service.post('/token/refresh/', {
            refresh: localStorage.getItem("retoken")
          })
          if (res.code == 200) {
            let token = res.data.result;
            localStorage.setItem("token", token.access);
            localStorage.setItem("retoken", token.refresh);
            error.response.config.baseURL = ''
            error.response.config.headers['Authorization'] = 'Bearer  ' + localStorage.getItem("token")
            return service(error.response.config)
          } else {
            router.push({
              path: "/login",
            })
            localStorage.clear();
            Notification.error({
              title: '认证失败',
              message: '当前身份认证信息已失效,请您重新登录',
              duration: 0
            });
          }
        } catch (error) {
          router.push({
            path: "/login",
          })
          localStorage.clear();
          Notification.error({
            title: '认证失败',
            message: '身份认证失败,请您重新登录,失败原因:' + error.message,
            duration: 0
          });
        }

        break
      case 404:
        Notification.error({
          title: '404错误',
          message: '服务器请求错误,请联系管理员或稍后重试。错误状态码:404',
        });
        break
      default:
        Notification.error({
          title: '请求错误',
          message: '服务器请求错误,请联系管理员或稍后重试',
        });
        break;
    }
    if (loadingInstance) {
      loadingInstance.close();
      NProgress.done();
    }
    return Promise.reject(error);

  }
)

// 文件下载通用方式
export const requestFile = axios.create({
  baseURL,
  timeout: 0, //关闭超时时间
});

requestFile.interceptors.request.use((config) => {
  config.headers['Authorization'] = 'Bearer  ' + localStorage.getItem("token") //携带的请求头
  config.responseType = 'blob';
  loadingInstance = Loading.service({
    lock: true,
    text: '正在下载,请稍候...',
    spinner: 'el-icon-loading',
    background: 'rgba(0, 0, 0, 0.3)'
  })
  return config;
});

requestFile.interceptors.response.use(
  (response) => {
    let res = response.data;
    if (loadingInstance) {
      loadingInstance.close()
    }
    // const contentType = response.headers['content-type'];//获取返回的数据类型
    let blob = new Blob([res], {
      type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" //文件下载以及上传类型为 .xlsx
    });
    let url = window.URL.createObjectURL(blob);
    // 创建一个链接元素
    let link = document.createElement('a');
    link.href = url;
    link.download = '产品列表.xlsx'; // 自定义文件名
    link.click();


  },
  (err) => {
    Message({
      message: '操作失败,请联系管理员',
      type: 'error',
    })
    if (loadingInstance) {

      loadingInstance.close()
    }
  }
);


export default service;