axios 拦截器实现用户无感刷新 access_token

概述

公司网站登录过期时间都通常有长有短(token 过期时间),有的很短(几个小时),但又想让经常活跃的用户不再次登录,于是才有这样需求,避免了用户再次输入账号密码登录。

为什么要专门用一个 refresh_token 去更新 access_token 呢?首先access_token会关联一定的用户权限,如果用户授权更改了,这个access_token也是需要被刷新以关联新的权限的,如果没有 refresh_token,也可以刷新 access_token,但每次刷新都要用户输入登录用户名与密码,多麻烦。有了 refresh_ token,可以减少这个麻烦,客户端直接用 refresh_token 去更新 access_token,无需用户进行额外的操作。

  • 需求
  1. access_token过期的时候,要用refresh_token去请求获取新的access_token,前端需要做到用户无感知的刷新access_token。比如用户发起一个请求时,如果判断access_token已经过期,那么就先要去调用刷新 token 接口拿到新的access_token,再重新发起用户请求。
  2. 如果同时发起多个用户请求,第一个用户请求去调用刷新 token 接口,当接口还没返回时,其余的用户请求也依旧发起了刷新 token 接口请求,就会导致多个请求,这些请求都要合理处理

思路

写在响应拦截器里,拦截返回后的数据。先发起用户请求,如果接口返回access_token过期,先刷新access_token,再进行一次重试。

  • 优点:无需判断时间
  • 缺点: 会消耗多一次 http 请求

实现

这里使用 axios,其中做的是请求后拦截,所以用到的是 axios 的响应拦截器

方法介绍

有用到cookie,也可以不用,目前localstorage用的最多,根据自己项目决定。

  • @util/auth.js
dart 复制代码
import Cookies from 'js-cookie'

const TOKEN_KEY = 'access_token'
const REGRESH_TOKEN_KEY = 'refresh_token'
export const REFRESH_TOKEN_STATUS_CODE='402'
export const REFRENSH_UIR='/refresh-token'
export const getToken = () => Cookies.get(TOKEN_KEY)

export const setToken = (token, params = {}) => {
  Cookies.set(TOKEN_KEY, token, params)
}

export const setRefreshToken = token => {
  Cookies.set(REGRESH_TOKEN_KEY, token)
}
  • request.js
javascript 复制代码
import axios from 'axios'
import { getToken, setToken, getRefreshToken,REFRESH_TOKEN_STATUS_CODE,REFRENSH_UIR } from '@util/auth'

// 刷新 access_token 的接口
const refreshToken = () => {
  return instance.post(REFRENSH_UIR, { refresh_token: getRefreshToken() }, true)
}

const instance = axios.create({
  baseURL: 'xxxx.aa/cc',
  timeout: 30000,
  headers: {
    'Content-Type': 'application/json',
  },
})

instance.interceptors.response.use(
  response => {
    return response
  },
  error => {
    if (!error.response) {
      return Promise.reject(error)
    }
    // token 过期或无效,在此处理逻辑
    return Promise.reject(error)
  }
)

// 给请求头添加 access_token
const setHeaderToken = isNeedToken => {
  const accessToken = isNeedToken ? getToken() : null
  if (isNeedToken) {
    // api 请求需要携带 access_token
    if (!accessToken) {
      console.log('不存在 access_token 则跳转回登录页')
    }
    instance.defaults.headers.common.Authorization = `Bearer ${accessToken}`
  }
}

// 有些 api 并不需要用户授权使用,则不携带 access_token;默认不携带,需要传则设置第三个参数为 true
export const get = (url, params = {}, isNeedToken = false) => {
  setHeaderToken(isNeedToken)
  return instance({
    method: 'get',
    url,
    params,
  })
}

export const post = (url, params = {}, isNeedToken = false) => {
  setHeaderToken(isNeedToken)
  return instance({
    method: 'post',
    url,
    data: params,
  })
}

接下来改造 request.js 中 axios 的响应拦截器

javascript 复制代码
instance.interceptors.response.use(
  response => {
    return response
  },
  error => {
    if (!error.response) {
      return Promise.reject(error)
    }
    if (error.response.status === REFRESH_TOKEN_STATUS_CODE) {
      const { config } = error
      return refreshToken()
        .then(res => {
          const { access_token } = res.data
          setToken(access_token)
          config.headers.Authorization = `Bearer ${access_token}`
          return instance(config)
        })
        .catch(err => {
          console.log('登录状态已失效,请重新登录!')
          return Promise.reject(err)
        })
    }
    return Promise.reject(error)
  }
)

约定返回 402(根据情况定) 状态码表示access_token过期或者无效,如果用户发起一个请求后返回结果是access_token过期,则请求刷新access_token的接口。请求成功则进入then里面,重置配置,并刷新access_token并重新发起原来的请求。

但如果refresh_token也过期了,则请求也是返回 402。此时调试会发现函数进不到refreshToken()catch里面,那是因为refreshToken()方法内部是也是用了同个instance实例,重复响应拦截器 402 的处理逻辑,但该函数本身就是刷新access_token,故需要把该接口排除掉,即:

lua 复制代码
if (error.response.status === 401 && !error.config.url.includes('/auth/refresh')) {
}

上述代码就已经实现了无感刷新access_token了,当access_token没过期,正常返回;过期时,则 axios 内部进行了一次刷新 token 的操作,再重新发起原来的请求。

优化

防止多次刷新 token

如果 token 是过期的,那请求刷新access_token的接口返回也是有一定时间间隔,如果此时还有其他请求发过来,就会再执行一次刷新access_token的接口,就会导致多次刷新access_token。因此,我们需要做一个判断,定义一个标记判断当前是否处于刷新access_token的状态,如果处在刷新状态则不再允许其他请求调用该接口。

javascript 复制代码
let isRefreshing = false // 标记是否正在刷新 token
instance.interceptors.response.use(
  response => {
    return response
  },
  error => {
    if (!error.response) {
      return Promise.reject(error)
    }
    if (error.response.status === REFRESH_TOKEN_STATUS_CODE && !error.config.url.includes(REFRENSH_UIR)) {
      const { config } = error
      if (!isRefreshing) {
        isRefreshing = true
        return refreshToken()
          .then(res => {
            const { access_token } = res.data
            setToken(access_token)
            config.headers.Authorization = `Bearer ${access_token}`
            return instance(config)
          })
          .catch(err => {
            console.log('登录状态已失效,请重新登录!')
            return Promise.reject(err)
          })
          .finally(() => {
            isRefreshing = false
          })
      }
    }
    return Promise.reject(error)
  }
)

同时发起多个请求的处理

上面做法还不够,因为如果同时发起多个请求,在 token 过期的情况,第一个请求进入刷新 token 方法,则其他请求进去没有做任何逻辑处理,单纯返回失败,最终只执行了第一个请求,这显然不合理。

比如同时发起三个请求,第一个请求进入刷新 token 的流程,第二个和第三个请求需要存起来,等到 token 更新后再重新发起请求。

在此,我们定义一个数组requests,用来保存处于等待的请求,之后返回一个Promise,只要不调用resolve方法,该请求就会处于等待状态,则可以知道其实数组存的是函数;等到 token 更新完毕,则通过数组循环执行函数,即逐个执行 resolve 重发请求。

javascript 复制代码
let isRefreshing = false // 标记是否正在刷新 token
let requests = [] // 存储待重发请求的数组

instance.interceptors.response.use(
  response => {
    return response
  },
  error => {
    if (!error.response) {
      return Promise.reject(error)
    }
    if (error.response.status === REFRESH_TOKEN_STATUS_CODE && !error.config.url.includes(REFRENSH_UIR)) {
      const { config } = error
      if (!isRefreshing) {
        isRefreshing = true
        return refreshToken()
          .then(res => {
            const { access_token } = res.data
            setToken(access_token)
            config.headers.Authorization = `Bearer ${access_token}`
            // token 刷新后将数组的方法重新执行
            requests.forEach(cb => cb(access_token))
            requests = [] // 重新请求完清空
            return instance(config)
          })
          .catch(err => {
            console.log('登录状态已失效,请重新登录!')
            return Promise.reject(err)
          })
          .finally(() => {
            isRefreshing = false
          })
      } else {
        // 返回未执行 resolve 的 Promise
        return new Promise(resolve => {
          // 用函数形式将 resolve 存入,等待刷新后再执行
          requests.push(token => {
            config.headers.Authorization = `Bearer ${token}`
            resolve(instance(config))
          })
        })
      }
    }
    return Promise.reject(error)
  }
)

最终 request.js 代码

js 复制代码
import axios from 'axios'
import { getToken, setToken, getRefreshToken ,REFRESH_TOKEN_STATUS_CODE,REFRENSH_UIR} from '@util/auth'

// 刷新 access_token 的接口
const refreshToken = () => {
  return instance.post(REFRENSH_UIR, { refresh_token: getRefreshToken() }, true)
}

// 创建 axios 实例
const instance = axios.create({
  baseURL: process.env.GATSBY_API_URL,
  timeout: 30000,
  headers: {
    'Content-Type': 'application/json',
  },
})

let isRefreshing = false // 标记是否正在刷新 token
let requests = [] // 存储待重发请求的数组

instance.interceptors.response.use(
  response => {
    return response
  },
  error => {
    if (!error.response) {
      return Promise.reject(error)
    }
    if (error.response.status === REFRESH_TOKEN_STATUS_CODE && !error.config.url.includes(REFRENSH_UIR)) {
      const { config } = error
      if (!isRefreshing) {
        isRefreshing = true
        return refreshToken()
          .then(res => {
            const { access_token } = res.data
            setToken(access_token)
            config.headers.Authorization = `Bearer ${access_token}`
            // token 刷新后将数组的方法重新执行
            requests.forEach(cb => cb(access_token))
            requests = [] // 重新请求完清空
            return instance(config)
          })
          .catch(err => {
            console.log('登录状态已失效,请重新登录!')
            return Promise.reject(err)
          })
          .finally(() => {
            isRefreshing = false
          })
      } else {
        // 返回未执行 resolve 的 Promise
        return new Promise(resolve => {
          // 用函数形式将 resolve 存入,等待刷新后再执行
          requests.push(token => {
            config.headers.Authorization = `Bearer ${token}`
            resolve(instance(config))
          })
        })
      }
    }
    return Promise.reject(error)
  }
)

// 给请求头添加 access_token
const setHeaderToken = isNeedToken => {
  const accessToken = isNeedToken ? getToken() : null
  if (isNeedToken) {
    // api 请求需要携带 access_token
    if (!accessToken) {
      console.log('不存在 access_token 则跳转回登录页')
    }
    instance.defaults.headers.common.Authorization = `Bearer ${accessToken}`
  }
}

// 有些 api 并不需要用户授权使用,则无需携带 access_token;默认不携带,需要传则设置第三个参数为 true
export const get = (url, params = {}, isNeedToken = false) => {
  setHeaderToken(isNeedToken)
  return instance({
    method: 'get',
    url,
    params,
  })
}

export const post = (url, params = {}, isNeedToken = false) => {
  setHeaderToken(isNeedToken)
  return instance({
    method: 'post',
    url,
    data: params,
  })
}
相关推荐
tager3 小时前
🔥3行代码搞定全局代理!告别插件依赖的极简方案
前端·fiddler·charles
程序员码歌4 小时前
【零代码AI编程实战】AI灯塔导航-成果展示篇
前端·ai编程·cursor
gnip4 小时前
前端实现即时通讯,常用的技术
前端
烛阴4 小时前
告别 any!用联合类型打造更灵活、更安全的 TS 代码
前端·typescript
excel5 小时前
全面解析 JavaScript 类继承:方式、优缺点与应用场景
前端
用户21411832636025 小时前
dify案例分享-100% 识别率!发票、汇票、信用证全搞定的通用票据识别工作流
前端
拾光拾趣录7 小时前
基础 | HTML语义、CSS3新特性、浏览器存储、this、防抖节流、重绘回流、date排序、calc
前端·面试
小小小小宇7 小时前
前端监测用户卡顿之INP
前端
小小小小宇7 小时前
监测用户在浏览界面过程中的卡顿
前端