前端vue实现 refresh_token刷新token

10,484 阅读4分钟

简介

目前很多项目都使用的是token机制,考虑到安全问题,一般会返回刷新token 和 使用的token、token过期时间

当使用token失效或者即将失效的时候,我们需要用刷新token发现ajax请求去获取一个新的token来代替之前使用的token。由于ajax是异步的,请求新的token需要一定的时间,在此时我们去请求接口,就会出现用旧token发送请求,导致401鉴权失败问题。

解决思路

在网上看的资料,感觉思路都大致相同,分两种情况

  • 1.在请求拦截中,拦截刷新token时,将config配置缓存,刷新成功后重新发起请求
  • 2.在响应拦截中,当token快过期的时候,将接口缓存,token成功刷新后再发送请求

我们缓存接口就需要用到promise了,axios拦截器内部其实是用Promise.then来获取我们拦截器中返回的数据。平常我们在请求拦截器中会return一个config,但其实return一个Promise.resolve(config)也是可以的。

不了解promise的朋友可以看阮一峰老师的文章 es6.ruanyifeng.com/#docs/promi…

代码大致如下

第一种方法(请求拦截)

  state: {
    token: getToken(),
    refresh_token: '',
    expires_time: '' // token保质期
  },
  mutations: {
    SET_TOKEN: (state, token) => {
      state.token = token
    },
    SET_REFRESH_TOKEN: (state, data) => { // 保存延续token
      state.refresh_token = data
    },
    SET_EXPIRES_TIME (state, data) { // 保存token过期时间
      let NOW_DATE = parseInt(new Date().getTime() / 1000)// 保存当前登陆时间
      state.expires_time = data + NOW_DATE
    }
  },
  actions: {
    loginIn ({ commit, state }, userInfo) {
      commit('SET_TOKEN', userInfo.access_token)
      commit('SET_REFRESH_TOKEN', userInfo.refresh_token)// 保存延续token
      commit('SET_EXPIRES_TIME', userInfo.expires_in)// 保存token过期时间
      Cookies.set('access_token', userInfo.access_token)
     }
    }

axios拦截器中

const isTokenExpired = () => { // 验证当前token是否过期
  let expireTime = store.state.user.expires_time
  if (expireTime) {
    let nowTime = parseInt(new Date().getTime() / 1000)
    let willExpired = (expireTime - nowTime) < 60// 如果超过60秒重新获取token
    return willExpired
  }
  return false
}
// 是否正在刷新的标记 -- 防止重复发出刷新token接口--节流阀
let isRefreshing = false
// 失效后同时发送请求的容器 -- 缓存接口
let subscribers = []
// 刷新 token 后, 将缓存的接口重新请求一次
function onAccessTokenFetched (newToken) {
  subscribers.forEach((callback) => {
    callback(newToken)
  })
  // 清空缓存接口
  subscribers = []
}
// 添加缓存接口
function addSubscriber (callback) {
  subscribers.push(callback)
}

service.interceptors.request.use(
  config => {
    config.headers = {
      'X-Requested-With': 'XMLHttpRequest',
      'Accept': 'application/json',
      'Authorization': 'Basic ' + cloudConfig.Authorization,
      'Content-Type': 'application/json; charset=UTF-8'
    }
    if (store.getters.token) {
      config.headers['Authorization'] = 'Bearer ' + getToken()
    }
      if (isTokenExpired() && !config.url.includes('/oauth/token')) { // 如果token快过期了
        if (!isRefreshing) { // 控制重复获取token
          isRefreshing = true
          let formData = new FormData()
          formData.append('grant_type', 'refresh_token')
          formData.append('refresh_token', store.state.user.refresh_token)
          axios({
            url: '/identity/oauth/token',
            method: 'post',
            data: formData,
            baseURL: cloudConfig.api_path,
            timeout: cloudConfig.api_timeout,
            headers: {
              'Authorization': 'Basic ' + cloudConfig.Authorization
            }
          }).then(res => {
            isRefreshing = false
            // console.log(res)
            if (res.status === 200) {
              store.dispatch('loginIn', res.data)
              onAccessTokenFetched(res.data.access_token)
            }
          }).catch(() => {
            router.push({ path: '/login' })// 失败就跳转登陆
            isRefreshing = false
          })
        }

        // 将其他接口缓存起来 -- 这个Promise函数很关键
        const retryRequest = new Promise((resolve) => {
          // 这里是将其他接口缓存起来的关键, 返回Promise并且让其状态一直为等待状态,
          // 只有当token刷新成功后, 就会调用通过addSubscriber函数添加的缓存接口,
          // 此时, Promise的状态就会变成resolve
          addSubscriber((newToken) => {
            // 表示用新的token去替换掉原来的token
            config.headers.Authorization = 'Bearer ' + newToken
            // 替换掉url -- 因为baseURL会扩展请求url
            config.url = config.url.replace(config.baseURL, '')
            // 返回重新封装的config, 就会将新配置去发送请求
            resolve(config)
          })
        })
        return retryRequest
      }
    return config
  },
  error => {
    console.log(error) // for debug
    return Promise.reject(error)
  }
)

service.interceptors.response.use(
  response => {
    const res = response.data
    -------忽略以下代码
    )

第二种方法(响应拦截)

其实和第一种方法大致差不多,只需要将retryRequest函数返回值改成axios

axios拦截器中

// 是否正在刷新的标记 -- 防止重复发出刷新token接口--节流阀
let isRefreshing = false
// 失效后同时发送请求的容器 -- 缓存接口
let subscribers = []
// 刷新 token 后, 将缓存的接口重新请求一次
function onAccessTokenFetched (newToken) {
  subscribers.forEach((callback) => {
    callback(newToken)
  })
  // 清空缓存接口
  subscribers = []
}
// 添加缓存接口
function addSubscriber (callback) {
  subscribers.push(callback)
}

service.interceptors.request.use(
  config => {
    config.headers = {
      'X-Requested-With': 'XMLHttpRequest',
      'Accept': 'application/json',
      'Authorization': 'Basic ' + cloudConfig.Authorization,
      'Content-Type': 'application/json; charset=UTF-8'
    }
    if (store.getters.token) {
      config.headers['Authorization'] = 'Bearer ' + getToken()
    }
    return config
  },
  error => {
    console.log(error) // for debug
    return Promise.reject(error)
  }
)

service.interceptors.response.use(
  response => {
    const res = response.data
      if ((isTokenExpired() || response.responseStatus === 401) && !response.config.url.includes('/oauth/token')) { // 如果token快过期了
        if (!isRefreshing) { // 控制重复获取token
          isRefreshing = true
          let formData = new FormData()
          formData.append('grant_type', 'refresh_token')
          formData.append('refresh_token', store.state.user.refresh_token)
          axios({
            url: '/identity/oauth/token',
            method: 'post',
            data: formData,
            baseURL: cloudConfig.api_path,
            timeout: cloudConfig.api_timeout,
            headers: {
              'Authorization': 'Basic ' + cloudConfig.Authorization
            }
          }).then(res => {
            isRefreshing = false
            // console.log(res)
            if (res.status === 200) {
              store.dispatch('loginIn', res.data)
              onAccessTokenFetched(res.data.access_token)
            }
          }).catch(() => {
            router.push({ path: '/login' })// 失败就跳转登陆
            isRefreshing = false
          })
        }

        // 将其他接口缓存起来 
        const retryRequest = new Promise((resolve) => {
          // 返回Promise并且让其状态一直为等待状态,
          // 只有当token刷新成功后, 就会调用通过addSubscriber函数添加的缓存接口,
          // 此时, Promise的状态就会变成resolve
          addSubscriber((newToken) => {
            // 表示用新的token去替换掉原来的token
            response.config.headers.Authorization = 'Bearer ' + newToken
            // 替换掉url -- 因为baseURL会扩展请求url
            response.config.url = response.config.url.replace(response.config.baseURL, '')
            // 用重新封装的config去请求, 就会将重新请求后的返回
            resolve(service(response.config))
          })
        })
        return retryRequest
      }
    -------忽略以下代码
    )

文章还有需改进的地方,只提供大概思路