同時多個axios請求怎麼實現無痛刷新token

需求

最近遇到個需求:前端登陸後,後端返回token和refresh_token,當token過時時用舊refresh_token去獲取新的token,前端須要作到無痛刷新token,即請求刷新token時要作到用戶無感知。前端

需求解析

當用戶發起一個請求時,判斷token是否已過時,若已過時則先調refreshToken接口,拿到新的token後再繼續執行以前的請求。vue

這個問題的難點在於:當同時發起多個請求,而刷新token的接口還沒返回,此時其餘請求該如何處理?接下來會按部就班地分享一下整個過程。ios

實現

這裏會使用axios來實現,以上方法是請求後攔截,因此會使用axios.interceptors.response.use()方法。vue-router

首先說明一下,項目中的token是存在localStorage中的。element-ui

如何防止屢次刷新token 若是refreshToken接口還沒返回,此時再有一個過時的請求進來,上面的代碼就會再一次執行refreshToken,這就會致使屢次執行刷新token的接口,所以須要防止這個問題。咱們能夠在request.js中用一個flag來標記當前是否正在刷新token的狀態,若是正在刷新則再也不調用刷新token的接口。axios

這樣子就能夠避免在刷新token時再進入方法了。可是這種作法是至關於把其餘失敗的接口給捨棄了,假如同時發起兩個請求,且幾乎同時返回,第一個請求確定是進入了refreshToken後再重試,而第二個請求則被丟棄了,還是返回失敗,因此接下來還得解決其餘接口的重試問題。後端

同時發起兩個或以上的請求時,其餘接口如何重試 兩個接口幾乎同時發起和返回,第一個接口會進入刷新token後重試的流程,而第二個接口須要先存起來,而後等刷新token後再重試。一樣,若是同時發起三個請求,此時須要緩存後兩個接口,等刷新token後再重試。因爲接口都是異步的,處理起來會有點麻煩。api

當第二個過時的請求進來,token正在刷新,咱們先將這個請求存到一個數組隊列中,想辦法讓這個請求處於等待中,一直等到刷新token後再逐個重試清空請求隊列。 那麼如何作到讓這個請求處於等待中呢?爲了解決這個問題,咱們得藉助Promise。將請求存進隊列中後,同時返回一個Promise,讓這個Promise一直處於Pending狀態(即不調用resolve),此時這個請求就會一直等啊等,只要咱們不執行resolve,這個請求就會一直在等待。當刷新請求的接口返回來後,咱們再調用resolve,逐個重試。最終代碼:跨域

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 }
複製代碼
相關文章
相關標籤/搜索