axios 请求拦截中 刷新token

608 阅读3分钟

一.场景

现有功能为token过期后跳转登录页重新登录,需要实现token过期后无感刷新

后端更新登录接口数据,以前只给了token,改为

新增数据作用
accessToken原token
accessTokenExpiryaccesToken有效日期的时间戳,有效时间一天(例)
refreshToken用来刷新accesToken
refreshTokenExpiryrefreshToken有效日期的时间戳,有效时间三十天(例)

二.思路

以前是判断接口返回token已过期的错误信息,未过期继续,过期则会跳转
所以就有了两种思路:
   1.请求拦截中处理
   2.相应拦截中处理
个人认为在相应拦截中处理会浪费一次请求,所以选择了在在请求拦截中处理

三.实现过程

一个很重要的点:Promise可以被另一个Promise锁定

下面是做的一些准备和初步实现:

//模拟数据
let mock = {
  accessToken: '123',
  accessTokenExpiry: true,
  refreshToken: 'asd',
  refreshTokenExpiry: false
}

//模拟刷新接口
function refreshApi() {
  return request({
    url: '/refresh',
    method: 'get'
  })
}

//白名单防止死循环
const whiteList = ['/refresh']

请求拦截
request.interceptors.request.use(config => {
  // 判断白名单
  if (whiteList.some(item => item === config.url)) {
    return config
  }

  config.headers.token = mock.accessToken

  // 判断accessToken是否过期,refreshToken是否过期
  if (mock.accessTokenExpiry && !mock.refreshTokenExpiry) {
    console.log('accessTokenExpiry已过期,refreshToken未过期')

    return refreshApi().then(res => {
      mock.accessToken = res.data + ''
      mock.accessTokenExpiry = true
      config.headers.token = mock.accessToken
      return config
    }
  } else {
    return config
  }
})

  这样确实是实现了在请求拦截中判断并刷新token,但是只能用在单个请求,如果用户进入的页面一次会发出多个请求,会出现一个请求刷新一次token的情况,所以做出以下变化:
  1.新增变量refreshIng,用来判断当前是否已经开始刷新token
  2.准备一个数组,将每个拦截器返回的promise的resolve封装在回调函数中依次存放在数组中,像这样:

let list = []
request.interceptors.request.use(config => {
 ...
    return new Promise(reslove => {
      // 因为return 的Promise一直在等resolve,所以这个拦截器暂时被锁定了,
      let cb = token => {
        config.headers.token = token
        reslove(config)
      }
      list.push(cb)
    })
 ...
})

  3.当刷新token的接口返回时,调用并更改list里每一个config所携带的token,并且reslove

request.interceptors.request.use(config => {
 ...
    // 判断是否已经在刷新token
    if (!refreshIng) {
      refreshIng = true

      refreshApi()
          .then(res => {
            mock.accessToken = res.data + ''
            mock.accessTokenExpiry = true
            refreshIng = false
            return mock.accessToken
          })
          .then(token=>{
           // 更改token,给每个pending中的Promise 返回resolve
           list.forEach(cb => cb(token))
           // 清空list
           list.length = 0
          })
    }
 ...
})

  改完之后的拦截器完整代码如下:

let mock = {
  accessToken: '123',
  accessTokenExpiry: true,
  refreshToken: 'asd',
  refreshTokenExpiry: false
}

function refreshApi() {
  return request({
    url: '/refresh',
    method: 'get'
  })
}

const whiteList = ['/refresh']

let refreshIng = false

let list = []

request.interceptors.request.use(config => {
  // 判断白名单
  if (whiteList.some(item => item === config.url)) {
    return config
  }

  config.headers.token = mock.accessToken

  // 判断accessToken是否过期,refreshToken是否过期
  if (mock.accessTokenExpiry && !mock.refreshTokenExpiry) {
    console.log('accessTokenExpiry已过期,refreshToken未过期')

    // 判断是否已经在刷新token
    if (!refreshIng) {
      refreshIng = true

      refreshApi()
        .then(res => {
          mock.accessToken = res.data + ''
          mock.accessTokenExpiry = true
          refreshIng = false
          return mock.accessToken
        })
        .then(res => {
          // 更改token,给每个pending中的Promise 返回resolve
          list.forEach(fn => fn(res))
          // 清空list
          list.length = 0
        })
    }

    return new Promise(reslove => {
      // 因为return 的Promise一直在等resolve,所以这个拦截器暂时被锁定了
      let fn = token => {
        config.headers.token = token
        reslove(config)
      }
      list.push(fn)
    })
  } else {
    return config
  }
})

五.结果

  实现了在refreshToken有效期内的无感刷新,提升了用户体验。
  学习了Promise和如何在本地用express在本地开服务,真不错。

参考文章:www.jianshu.com/p/115b4c79a…