Как axios использует обещание для безболезненного обновления токена

axios

необходимость

Недавно столкнулся с требованием: после логина фронтенда бэкенд возвращаетtokenиtoken有效时间, когда срок действия токена истекает, для получения нового токена требуется старый токен, а внешний интерфейс необходимо безболезненно обновить.token, то есть пользователь не должен знать, запрашивая обновление токена.

Анализ требований

Когда пользователь инициирует запрос, оценивается, истек ли срок действия токена, и если срок его действия истек, он будет вызван первым.refreshTokenинтерфейс, получить новый токен, а затем продолжить выполнение предыдущего запроса.

Сложность этой проблемы заключается в следующем: когда несколько запросов инициируются одновременно, а интерфейс для обновления токена не вернулся, как в это время должны обрабатываться другие запросы? Далее я расскажу весь процесс шаг за шагом.

Реализовать идеи

Поскольку серверная часть возвращает действительное время токена, есть два способа:

метод первый:

Перехватывайте каждый запрос до того, как запрос будет инициирован, и определяйте, истек ли срок действия токена. Если он истек, запрос будет приостановлен, а токен будет обновлен перед продолжением запроса.

Способ второй:

Вместо перехвата перед запросом перехватывайте возвращаемые данные. Сначала инициируйте запрос, а после того, как интерфейс вернет срок действия, сначала обновите токен, а затем повторите попытку.

Сравнение двух методов

метод первый

  • Преимущества: перехват перед запросами может сэкономить запросы и трафик.
  • Недостатки: Бэкенд должен предоставить дополнительное поле времени истечения срока действия токена; используя оценку локального времени, если местное время подделано, особенно когда местное время медленнее, чем время сервера, перехват не удастся.

PS: Предполагается, что действительное время токена — это период времени, аналогичный кэшированному MaxAge, а не абсолютное время. Абсолютное время проблематично, когда серверное и местное время несовместимы.

Способ второй

  • Преимущества: нет необходимости в дополнительных полях срока действия токена, не нужно оценивать время.
  • Недостатки: это будет потреблять еще один запрос и потреблять трафик.

Подводя итог, можно сказать, что преимущества и недостатки методов 1 и 2 дополняют друг друга. Метод 1 имеет риск сбоя проверки (когда местное время подделывается, конечно, пользователь обычно не испытывает боли при изменении местного времени), метод 2 проще и грубее, подождите, пока срок действия сервера не истечет, и попробуйте еще раз, но он будет потреблять еще один запрос.

Здесь блогер выбираетСпособ второй.

выполнить

буду использовать здесьaxiosЧтобы достичь этого, первый метод заключается в перехвате перед запросом, поэтому он будет использоватьaxios.interceptors.request.use()Сюда;

Второй метод заключается в перехвате после запроса, поэтому он будет использоватьaxios.interceptors.response.use()метод.

Инкапсулировать базовый скелет axios

Прежде всего, поясню, что токен в проекте существуетlocalStorageсередина.request.jsОсновной скелет:

import axios from 'axios'

// 从localStorage中获取token
function getLocalToken () {
    const token = window.localStorage.getItem('token')
    return token
}


// 给实例添加一个setToken方法,用于登录后将最新token动态添加到header,同时将token保存在localStorage中
instance.setToken = (token) => {
  instance.defaults.headers['X-Token'] = token
  window.localStorage.setItem('token', token)
}

// 创建一个axios实例
const instance = axios.create({
  baseURL: '/api',
  timeout: 300000,
  headers: {
    'Content-Type': 'application/json',
    'X-Token': getLocalToken() // headers塞token
  }
})

// 拦截返回的数据
instance.interceptors.response.use(response => {
  // 接下来会在这里进行token过期的逻辑处理
  return response
}, error => {
  return Promise.reject(error)
})

export default instance

Это инкапсуляция общего инстанса axios в проекте.При создании инстанса поместите локально существующий токен в шапку, а потом экспортируйте его для вызова. Следующий шаг — как перехватить возвращенные данные.

реализация перехвата instance.interceptors.response.use

Серверный интерфейс обычно имеет согласованную структуру данных, такую ​​как:

{code: 1234, message: 'token过期', data: {}}

Поскольку я здесь, внутреннее соглашение заключается в том, когдаcode === 1234По истечении срока действия токена необходимо обновить токен.

instance.interceptors.response.use(response => {
  const { code } = response.data
  if (code === 1234) {
    // 说明token过期了,刷新token
    return refreshToken().then(res => {
      // 刷新token成功,将最新的token更新到header中,同时保存在localStorage中
      const { token } = res.data
      instance.setToken(token)
      // 获取当前失败的请求
      const config = response.config
      // 重置一下配置
      config.headers['X-Token'] = token
      config.baseURL = '' // url已经带上了/api,避免出现/api/api的情况
      // 重试当前请求并返回promise
      return instance(config)
    }).catch(res => {
      console.error('refreshtoken error =>', res)
      //刷新token失败,神仙也救不了了,跳转到首页重新登录吧
      window.location.href = '/'
    })
  }
  return response
}, error => {
  return Promise.reject(error)
})

function refreshToken () {
    // instance是当前request.js中已创建的axios实例
    return instance.post('/refreshtoken').then(res => res.data)
}

Здесь важно отметить, что,response.configЭто конфигурация исходного запроса, но она была обработана,config.urlуже принесbaseUrl, поэтому его нужно удалить при повторной попытке, а токен тоже старый и его нужно обновить.

Вышеупомянутое в основном обеспечивает безболезненное обновление токена.Когда токен нормальный, он возвращается нормально.Когда срок действия токена истек, axios внутренне обновляет токен и повторяет попытку. Для вызывающей стороны токен обновления внутри axios является черным ящиком и незаметен, поэтому требование выполнено.

Проблемы и оптимизация

В приведенном выше коде все еще есть некоторые проблемы, и проблема множественных запросов не рассматривается, поэтому его необходимо дополнительно оптимизировать.

Как предотвратить несколько токенов обновления

Если интерфейс refreshToken не вернулся, и в это время приходит другой запрос с истекшим сроком действия, приведенный выше код снова выполнит refreshToken, что приведет к многократному обновлению токена интерфейсом, поэтому эту проблему необходимо предотвратить. мы можемrequest.jsиспользовать один вflagЧтобы отметить, обновляется ли токен в данный момент, если он обновляется, интерфейс для обновления токена не будет вызываться.

// 是否正在刷新的标记
let isRefreshing = false
instance.interceptors.response.use(response => {
  const { code } = response.data
  if (code === 1234) {
    if (!isRefreshing) {
      isRefreshing = true
      return refreshToken().then(res => {
        const { token } = res.data
        instance.setToken(token)
        const config = response.config
        config.headers['X-Token'] = token
        config.baseURL = ''
        return instance(config)
      }).catch(res => {
        console.error('refreshtoken error =>', res)
        window.location.href = '/'
      }).finally(() => {
        isRefreshing = false
      })
    }
  }
  return response
}, error => {
  return Promise.reject(error)
})

Таким образом, вы можете избежать повторного ввода метода при обновлении токена. Однако такой подход эквивалентен отказу от других отказавших интерфейсов.Если два запроса инициируются одновременно и возвращаются почти одновременно, первый запрос должен войти в refreshToken и затем повторить попытку, а второй запрос отбрасывается. по-прежнему возвращает ошибку, поэтому проблема повторных попыток других интерфейсов должна быть решена следующей.

Как другие интерфейсы повторяют попытку, когда два или более запросов инициируются одновременно?

Два интерфейса инициируются и возвращаются почти одновременно.Первый интерфейс войдет в процесс повторной попытки после обновления токена, в то время как второй интерфейс необходимо сначала сохранить, а затем повторить попытку после обновления токена. Точно так же, если три запроса инициируются одновременно, последние два интерфейса необходимо кэшировать в это время, а затем повторить попытку после обновления токена. Поскольку все интерфейсы являются асинхронными, это может быть немного громоздко.

Когда приходит второй запрос с истекшим сроком действия и токен обновляется, мы сначала сохраняем запрос в очереди массива и пытаемся удержать запрос в ожидании, пока токен не будет обновлен, а затем повторяем попытку очистить очередь запросов один за другим. Итак, как сделать этот запрос ожидающим? Чтобы решить эту проблему, мы должны использоватьPromise. После сохранения запроса в очереди вернитеPromise, пусть этоPromiseбыл вPendingstate (то есть не вызывая resolve), в это время запрос будет ждать и ждать, пока мы не выполним resolve, запрос будет ждать. Когда интерфейс запроса на обновление возвращается, мы вызываем разрешение и повторяем попытку один за другим. Окончательный код:

// 是否正在刷新的标记
let isRefreshing = false
// 重试队列,每一项将是一个待执行的函数形式
let requests = []

instance.interceptors.response.use(response => {
  const { code } = response.data
  if (code === 1234) {
    const config = response.config
    if (!isRefreshing) {
      isRefreshing = true
      return refreshToken().then(res => {
        const { token } = res.data
        instance.setToken(token)
        config.headers['X-Token'] = token
        config.baseURL = ''
        // 已经刷新了token,将所有队列中的请求进行重试
        requests.forEach(cb => cb(token))
        // 重试完了别忘了清空这个队列(掘金评论区同学指点)
        requests = []
        return instance(config)
      }).catch(res => {
        console.error('refreshtoken error =>', res)
        window.location.href = '/'
      }).finally(() => {
        isRefreshing = false
      })
    } else {
      // 正在刷新token,返回一个未执行resolve的promise
      return new Promise((resolve) => {
        // 将resolve放进队列,用一个函数形式来保存,等token刷新后直接执行
        requests.push((token) => {
          config.baseURL = ''
          config.headers['X-Token'] = token
          resolve(instance(config))
        })
      })
    }
  }
  return response
}, error => {
  return Promise.reject(error)
})

Что может быть более трудным для понимания, так это то, чтоrequestsВ этой очереди хранится функция, которая предотвращает выполнение разрешения, сначала сохраните его, а затем более удобно вызывать эту функцию, чтобы выполнить разрешение после обновления токена. На этом этапе проблема должна быть решена.

Окончательный полный код

import axios from 'axios'

// 从localStorage中获取token
function getLocalToken () {
    const token = window.localStorage.getItem('token')
    return token
}

// 给实例添加一个setToken方法,用于登录后将最新token动态添加到header,同时将token保存在localStorage中
instance.setToken = (token) => {
  instance.defaults.headers['X-Token'] = token
  window.localStorage.setItem('token', token)
}

function refreshToken () {
    // instance是当前request.js中已创建的axios实例
    return instance.post('/refreshtoken').then(res => res.data)
}

// 创建一个axios实例
const instance = axios.create({
  baseURL: '/api',
  timeout: 300000,
  headers: {
    'Content-Type': 'application/json',
    'X-Token': getLocalToken() // headers塞token
  }
})

// 是否正在刷新的标记
let isRefreshing = false
// 重试队列,每一项将是一个待执行的函数形式
let requests = []

instance.interceptors.response.use(response => {
  const { code } = response.data
  if (code === 1234) {
    const config = response.config
    if (!isRefreshing) {
      isRefreshing = true
      return refreshToken().then(res => {
        const { token } = res.data
        instance.setToken(token)
        config.headers['X-Token'] = token
        config.baseURL = ''
        // 已经刷新了token,将所有队列中的请求进行重试
        requests.forEach(cb => cb(token))
        requests = []
        return instance(config)
      }).catch(res => {
        console.error('refreshtoken error =>', res)
        window.location.href = '/'
      }).finally(() => {
        isRefreshing = false
      })
    } else {
      // 正在刷新token,将返回一个未执行resolve的promise
      return new Promise((resolve) => {
        // 将resolve放进队列,用一个函数形式来保存,等token刷新后直接执行
        requests.push((token) => {
          config.baseURL = ''
          config.headers['X-Token'] = token
          resolve(instance(config))
        })
      })
    }
  }
  return response
}, error => {
  return Promise.reject(error)
})

export default instance

Я надеюсь, что все должны помочь. Спасибо, что досмотрели до конца, спасибо за лайки ^_^.

Последующие обновления

Для реализации метода 1, пожалуйста, прочтите:Как axios использует обещание для безболезненного обновления токена (2)

Категории