необходимость
Недавно столкнулся с требованием: после логина фронтенда бэкенд возвращает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
был вPending
state (то есть не вызывая 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)