Как безболезненно обновить токен с помощью нескольких запросов axios одновременно

внешний интерфейс

нужно

Недавно столкнулся с требованием: после входа в систему серверная часть возвращает токен и refresh_token.Когда срок действия токена истекает, старый refresh_token используется для получения нового токена.Фронтенд должен безболезненно обновить токен , то есть при запросе на обновление токена пользователь должен быть в неведении.

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

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

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

выполнить

Здесь будет использоваться Axios, вышеописанный метод является перехватом после запроса, поэтому будет использоваться метод axios.interceptors.response.use().

Во-первых, токен в проекте хранится в localStorage.

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

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

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

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

import axios from 'axios'
import { Loading, Message, MessageBox } from 'element-ui'
import api from './api'
import { getToken, setToken, removeToken, getRefreshToken } from '../utils/cookies'

let UserModule = {
	RefreshToken: (data) => {
		setToken('Bearer ' + data.access_token, data.refresh_token)
	}
}

// 是否正在刷新的标记
let isRefreshing = false

// 重试队列,每一项将是一个待执行的函数形式
let retryRequests = []

const request = axios.create({
	baseURL: api.baseUrl,
	timeout: 50000,
	withCredentials: true // cookie跨域必备
})
// http request 拦截器 Request
request.interceptors.request.use(
	(config) => {
		if (getToken()) {
			config.headers['Authorization'] = getToken()
		}
		return config
	},
	(error) => {
		Promise.reject(error)
	}
)

// http response 拦截器 Response
request.interceptors.response.use(
	(response) => {
		// code == 0: 成功
		const res = response.data
		if (res.code !== 0) {
			if (res.message) {
				Message({
					message: res.message,
					type: 'error',
					duration: 5 * 1000
				})
			}
			return Promise.reject(res)
		} else {
			return response.data
		}
	},
	(error) => {
		if (!error.response) return Promise.reject(error)
		// 根据refreshtoken重新获取token
		// 5000系统繁忙
		// 5001参数错误
		// 1003该用户权限不足以访问该资源接口
		// 1004访问此资源需要完全的身份验证
		// 1001access_token无效
		// 1002refresh_token无效
		if (error.response.data.code === 1004 || error.response.data.code === 1001) {
			const config = error.config
			if (!isRefreshing) {
				isRefreshing = true
				return getRefreshTokenFunc()
					.then((res) => {
						// 重新设置token
						UserModule.RefreshToken(res.data.data)
						config.headers['Authorization'] = getToken()
						// 已经刷新了token,将所有队列中的请求进行重试
						// @ts-ignore
						retryRequests.forEach((cb) => cb(getToken()))
						// 重试完清空这个队列
						retryRequests = []
						// 这边不需要baseURL是因为会重新请求url,url中已经包含baseURL的部分了
						config.baseURL = ''
						return request(config)
					})
					.catch(() => {
						resetLogin()
					})
					.finally(() => {
						isRefreshing = false
					})
			} else {
				// 正在刷新token,返回一个未执行resolve的promise
				return new Promise((resolve) => {
					// 将resolve放进队列,用一个函数形式来保存,等token刷新后直接执行
					// @ts-ignore
					retryRequests.push((token: any) => {
						config.baseURL = ''
						config.headers['Authorization'] = token
						resolve(request(config))
					})
				})
			}
		} else if (error.response.data.code === 1002) {
			resetLogin()
		} else {
			Message({
				message: error.response.data.message,
				type: 'error',
				duration: 5 * 1000
			})
			return Promise.reject(error)
		}
	}
)
// 刷新token的请求方法
function getRefreshTokenFunc() {
	let params = {
		refresh_token: getRefreshToken() || ''
	}
	return axios.post(api.baseUrl + 'auth-center/auth/refresh_token', params)
}
function resetLogin(title = '身份验证失败,请重新登录!') {
	if (window.location.href.indexOf('/login') === -1) {
		MessageBox.confirm(title, '退出', {
			confirmButtonText: '重新登录',
			cancelButtonText: '取消',
			type: 'warning'
		}).then(() => {
			removeToken()
			location.reload() // To prevent bugs from vue-router
		})
	}
}
/**
 * []请求
 * @param params  参数
 * @param operation     接口
 */
function customRequest(url: string, method: any, data: any) {
	// service.defaults.headers['Content-Type']=contentType
	let datatype = method.toLocaleLowerCase() === 'get' ? 'params' : 'data'
	return request({
		url: url,
		method: method,
		[datatype]: data
	})
}

export { request, customRequest }