前端使用OAuth2.0的实现refreshToken无痛刷新

lxf2023-03-14 20:34:01

前端使用OAuth2.0的实现refreshToken无痛刷新

业务背景

现在项目登录鉴权一般使用的是 OAuth 2.0, 登录完成后会返回两个token,分别是 accessTokenrefreshToken > accessToken 我们在调接口的时候会带给服务端,作为用户凭证在, refreshToken 是用来刷新用户凭证的一个参数 > 目的是避免用户经常因为还在操作页面的时候 accessToken 过期,需要重新登录的较差体验

原理

  • 登录完成后会返回两个token,分别是 accessTokenrefreshToken 以及一个 expiresTime 过期时间
  • accessToken 作为用户凭,证调接口的时候会带给服务端
  • 前端会在拦截器中判断 当前时间是否超过了 expiresTime 过期时间
    • 超过了,就会调用刷新 token 的接口, refreshToken 将作为请求参数带到服务端,接口会返回新的 accessTokenrefreshTokenexpiresTime,后面的请求将使用新的 accessToken 作为用户凭证带给服务端
    • 未超过,直接使用 accessToken 作为用户凭证
  • 只要用户在一直操作,那么就算他的 accessToken 过期也不需要重新登录
  • 直到超过了 refreshToken 的过期时间(一般这个时间会比 accessToken 过期时间长很多,说明用户在这段时间内都没有进行操作),才会真正的过期

实现

项目将使用axios的库进行请求的发送和拦截

目录结构

src └─libs ├─axios │ ├─config.js │ ├─index.js │ ├─instance.js

config.js

/**
 * axios的公共配置
 */
const config = {
  baseURL: process.env.VUE_APP_BASE_API, // base_api
  // 公共请求头
  headers: {
    'Content-Type': 'application/json; charset=UTF-8',
  },
  timeout: 5000, // 超时时间
  // 默认的响应方式
  responseType: 'json'
}

export default config

instance.js

import axios from 'axios'
import config from './config'
import { Message } from 'element-ui'
import {
  getAccessToken,
  removeAccessToken,
  removeRefreshToken,
  removeExpiresTime
} from '@/utils/auth' //这里的函数主要是使用 localStorage 进行数据持久化
import router from '@/router'
import i18n from '@/lang'  // 国际化

/**
 * 创建一个独立的axios实例
 * 把常用的公共请求配置放这里添加
 */
const instance = axios.create(config)

/**
 * 请求拦截
 * 添加一些全局要带上的东西
 */
instance.interceptors.request.use(
  // 正常拦截
  (config) => {
    // 添加token
    const LOCAL_TOKEN = getAccessToken()
    if (LOCAL_TOKEN) {
      config.headers['Authorization'] = LOCAL_TOKEN
    }
    // 返回处理后的配置
    return Promise.resolve(config)
  },

  // 拦截失败
  (err) => Promise.reject(err)
)

/**
 * 返回拦截
 * 在这里解决数据返回的异常问题
 */
instance.interceptors.response.use(
  // 正常响应
  (res) => {
    if (res.status === 200) {
      return Promise.resolve(res.data)
    } else {
      return Promise.reject(res)
    }
  },
  // 异常响应(统一返回一个msg提示)
  (err) => {
    if (err.response && err.response.status) {
      switch (err.response.status) {
        // ...
        case 500:
          Message({
            message: i18n.t(`error.${err.response.data.code}`),
            duration: 1500,
            type: 'error'
          })
          if (err.response.data.code === 'TOKEN_INVALID') { //token 失效后的操作
            removeAccessToken()
            removeExpiresTime()
            removeRefreshToken()
            router.replace({
              path: '/login'
            })
          }
          break
        default:
          return
      }
      return Promise.reject(err.response)
    } else {
      try {
        Message({
          message: '服务器异常',
          duration: 1500,
          type: 'error'
        })
      } catch (e) {
        console.log(e)
      }
    }
  }
)

export default instance

index.js

import axios from './instance'
import {
  getAccessToken,
  getExpiresTime,
  setAccessToken,
  getRefreshToken,
  setRefreshToken,
  setExpiresTime
} from '@/utils/auth' //这里的函数主要是使用 localStorage 进行数据持久化
import { refresh } from '@/api/user' // refresh 的api

// 防止重复刷新的状态开关
let isRefreshing = false
// 被拦截的请求列表
let requests = []
/**
 * 请求拦截
 * 在这里要判断是否需要刷新token
 * 帮助用户自动延长登录有效期
 */
axios.interceptors.request.use((config) => {
  /**
   * 刷新token
   */
  // 计算token的剩余有效时间
  const OLD_TOKEN_EXP = getExpiresTime() || 0
  const NOW_TIMESTAMP = Date.now()
  const TIME_DIFF = OLD_TOKEN_EXP - NOW_TIMESTAMP
  // 判断本地是否有记录
  const HAS_LOCAL_TOKEN = !!getAccessToken()
  const HAS_LOCAL_TOKEN_EXP = !!OLD_TOKEN_EXP
  // 获取接口url
  const API_URL = config.url.split('?')[0] || ''
  // 非刷新请求、有本地记录、已过期,同时满足,才会进入刷新流程
  if (
    API_URL !== '/admin-users/refreshToken' && // 这个地方判断接口是否是刷新接口, api 地址需要一致
    HAS_LOCAL_TOKEN &&
    HAS_LOCAL_TOKEN_EXP &&
    TIME_DIFF <= 2000 // 这里的 2000 ms 是防止发送请求的时候没过期,但是接口到达服务器的时候过期了,根据实际情况进行调整
  ) {
    // 如果没有在刷新,则执行刷新
    if (!isRefreshing) {
      // 打开状态
      isRefreshing = true
      // 获取新的token
      const REFRESH_TOKEN = getRefreshToken() || ''
      // 请求刷新
      refresh(REFRESH_TOKEN)
        .then((res) => {
          // 存储token信息
          const data = res.data
          setAccessToken(data.accessToken)
          setRefreshToken(data.refreshToken)
          setExpiresTime(data.expiresTime)
          // 如果新的token存在,用新token继续之前的请求,然后重置队列
          if (data.accessToken) {
            config.headers['Authorization'] = data.accessToken
            requests.forEach((cb) => cb(config))
            requests = []
          }
          // 关闭状态,允许下次继续刷新
          isRefreshing = false
        })
        .catch(() => {
          isRefreshing = false
          requests = []
        })
    }
    // 并把刷新完成之前的请求都存储为请求队列
    return new Promise((resolve) => {
      requests.push(() => {
        resolve(config)
      })
    })
  }
  return Promise.resolve(config)
})

export default axios

参考资料

基于OAuth2.0的refreshToken前端刷新方案与演示demo