应用场景:在一个管理系统中,当用户登录进来后,我们期望用户在操作时,不会因为token过期而被迫登出。但token是有时效的,这时候我们就需要一个刷新token的操作来保障用户的登录状态;而当用户长时间未操作,则可以被登出。

1. 原理

  1. 登录,从后台获取到token(鉴权令牌),refresh_token(刷新token的令牌),expire_time(token的时效)。将这三个以及登录的时间点(loginTime)存储下来,以备使用。

  2. 用户操作中,向后台发送请求,每次请求时,将当前请求时间(currentTime)与loginTime和expire_time对比,
    即(currentTime-loginTime)得到的时间段即将接近或超出expire_time时,使用refresh_token去重新获取token。
    注:此处需要知道的是,refresh_token与token一样,都是有时效的。但refresh_token的时效必定长于token,这样token即便过期了,也不会影响refresh_token。因此只要用户在refresh_token的有效期内向后台发送请求,token就可以一直得到刷新。
    而用户长时间未操作,refresh_token也过期了,这时候就可以被正常登出。

  3. 使用refresh_token去重新获取token的操作实际上就是再次进行了一次登录操作,只不过这次的参数并非账密,而是refresh_token,
    并且这个操作用户是不知情的。每次登录获取到的鉴权信息都会覆盖上一次存储的鉴权信息,这样就会确保token和refresh_token一直都是最新的。

大体流程就是以上三个步骤循环。

2. 思路图

3. 核心代码

request.ts

import axios, { AxiosInstance } from 'axios'
import router from '@/router'
import store from '@/store'
import { message } from 'ant-design-vue'
import { getToken } from '@/utils/auth'
import { refreshToken } from '@/utils/refreshToken'

// create an axios instance
const service = axios.create({
  baseURL: '/api',
  timeout: 30000
})

// 请求拦截器
service.interceptors.request.use(
  (config: any) => {
    if (store.getters.token) {
      config.headers['Authorization'] = getToken()
    }
    // 登录,不校验token
    if (config.url.indexOf('/login') > -1) {
      return config
    } else {
      let interval = null
      let retry = new Promise((resolve, reject) => {
        const refreshFun = () => refreshToken().then(res => {
          // 判断是否刷新token,且成功了
          if (res === 'success') {
            console.log('刷新token!!!')
            config.headers['Blade-Auth'] = `Bearer ${getToken()}`
            // 挂起请求
            resolve(config)
          } else if (res === 'pending') {
            console.log('等待刷新token!!!')
            interval = setInterval(() => {
              refreshFun()
            }, 500)
          } else { // 不需要刷新token或刷新失败
            // 等待刷新成功后就不需要再刷新,此时需重新赋值新token
            if (interval) {
              console.log('等待成功!')
              config.headers['Blade-Auth'] = `Bearer ${getToken()}`
              clearInterval(interval)
            }
            // 挂起请求
            resolve(config)
          }

        })

        refreshFun()
      })
      return retry
    }
  },
  error => {
    return Promise.reject(error)
  }
)

// 返回拦截器
service.interceptors.response.use(
  response => {
    if (response.data.success) {
      // 若为登录接口,记录登录返回的时间
      if (response.config.url.indexOf('/login') > -1) {
        const time = String(new Date().getTime())
        localStorage.setItem('loginTime', time)
      }
      return Promise.resolve(response.data)
    }
    response.data.msg && message.error(response.data.msg)
    return Promise.reject(response.data.msg)
  },
  error => {
    if (error.response.status && error.response.status === 401) {
      store.dispatch('user/logout')
      router.push('/login')
    }
    error.response.data.msg && message.error(error.response.data.msg)
    return Promise.reject(error)
  }
)

export default service as AxiosInstance

refreshToken.ts

import store from '@/store'

export async function refreshToken() {
  const currentTime = new Date().getTime()
  const loginTime = Number(localStorage.getItem('loginTime'))
  const userInfo = localStorage.getItem('USER_INFO')

  if (loginTime && userInfo) {
    const { expires_in, refresh_token } = JSON.parse(localStorage.getItem('USER_INFO'))
    const splitTime = expires_in - (currentTime - loginTime) / 1000
    if (splitTime < 60) { // token过期时间小于1分钟时获取新token
      let params = {
        type: 'refresh_token',
        refresh_token: refresh_token,
      }
      const refreshTokenStatus = localStorage.getItem('refreshTokenStatus')
      // 确保同一时间段内只执行一次
      if (!refreshTokenStatus) {
        localStorage.setItem('refreshTokenStatus', 'true')
        await store.dispatch('user/login', params).catch(function(err) {
          return '' // 请求失败
        })
        // 请求成功,清除状态值
        localStorage.removeItem('refreshTokenStatus')
        return 'success'
      } else { // 正在获取token,不再重复请求
        return 'pending'
      }
    } else { // 未到过期时间,不请求
      return ''
    }
  }
}

4. 注意点

  1. 登录成功,在登录接口的返回拦截器里记录登录时间

  2. 登出时清除以上所有存储的鉴权信息

  3. 多接口并发请求,且此时token已经过期。只需要在第一个接口里去请求刷新token,后面的接口先挂起,等到拿到最新的token后,更新请求头,发送请求。
    做法:定义一个状态值,用来确保相近的时间段内不会重复请求刷新token

  4. 长时间未操作,refresh_token过期,用户登出

5. 总结

这是个人总结一种方式,且已经应用在实际的项目中,暂时未出现问题。其实刷新token的方式是多种多样的,例如另一种方式是直接在主页面写个定时器,定时刷新token,这样较为简便,但却不适用于本项目。因此我们需要基于实际情况选择合适的方式 。

 

Logo

快速构建 Web 应用程序

更多推荐