雙token無痛刷新機制主要是由一個accessToken和一個refreshToken實現的,請求接口的時候使用accessToken,一旦accessToken過期,立刻用refreshToken請求刷新token接口,拿到accessToken、refreshToken存起來,然後使用accessToken請求接口。
這其中有幾個點需要注意:
- 當前過期的token如何處理?
- 過期token刷新後,當前失敗的接口怎麼處理?
- 在刷新token的過程中,新進來的請求如何處理?
- 如果refreshToken也過期了,如何處理?
帶着這幾個問題開始解決
這裏我先展示token的操作文件,本文會用到
這裏展示一個測試目錄結構:
api.ts——api接口文件
import request from "@/views/test/request";
// 登錄
export const getTestTokenAPI = () => {
return request({
url: "/mock/test/getTestToken",
method: "get"
});
};
// 刷新token
export const getTestTokenRefreshAPI = (data: any) => {
return request({
url: "/mock/test/getTestTokenRefresh",
method: "post",
data,
_isRefreshToken: true // 添加自定義屬性,標記當前接口為刷新token接口
} as any);
};
// 其它請求
export const getTestRequestAPI = (params: any) => {
return request({
url: "/mock/test/getTestRequest",
method: "get",
params
});
};
在請求的時候添加的自定義屬性,如:_isRefreshToken, 在axios的響應攔截器的config屬性裏可以獲取到,例如:response.config._isRefreshToken
auth.ts——token的獲取和設置文件
import { useLocalStorage } from "@vueuse/core";
// https://vueuse.org.cn/core/uselocalstorage/
// 響應式 LocalStorage
const accessToken = useLocalStorage("accessToken", "");
const refreshToken = useLocalStorage("refreshToken", "");
// accessToken是否存在
function isAccessToken() {
return !!accessToken.value;
}
// refreshToken是否存在
function isRefreshToken() {
return !!refreshToken.value;
}
// 獲取accessToken
function getAccessToken() {
return accessToken.value;
}
// 獲取refreshToken
function getRefreshToken() {
return refreshToken.value;
}
// 設置雙token
function setToken(newToken: { accessToken: string; refreshToken: string }) {
accessToken.value = newToken.accessToken;
refreshToken.value = newToken.refreshToken;
}
// 清空雙token
function clearToken() {
accessToken.value = "";
refreshToken.value = "";
}
export { isAccessToken, isRefreshToken, getAccessToken, getRefreshToken, setToken, clearToken };
refreshToken.ts——刷新token的邏輯處理文件
import service from "@/views/test/request";
import { getTestTokenRefreshAPI } from "@/views/test/api";
import { setToken, getRefreshToken } from "@/views/test/auth";
// 請求隊列
let failedQueue: any[] = [];
/**
* 刷新token
* 該函數會調用刷新token的接口,刷新成功後會將新的token設置到localStorage中
*/
export async function refreshToken() {
// 調用刷新token接口
let res = await getTestTokenRefreshAPI({ refreshToken: getRefreshToken() });
let params = {
accessToken: res.data.accessToken,
refreshToken: res.data.refreshToken
};
setToken(params);
}
/**
* 將請求添加到隊列
* @param response 需要添加的請求
* @returns {Promise<any>} 隊列中的請求結果
*/
export function addRequestToQueue(response: any) {
return new Promise((resolve, reject) => {
failedQueue.push({
resolve: async (newToken: string) => {
// 用新Token重試請求
response.config.headers["Authorization"] = "Bearer " + newToken;
let res = await service.request(response.config);
resolve(res);
},
reject: (err: any) => reject(err)
});
});
}
/**
* 請求隊列中的接口,只有在token刷新成功或失敗後才會調用
* 刷新失敗的調用方式:processQueue(error)
* 刷新成功的調用方式:processQueue(null, newToken)
* @param error 刷新失敗的錯誤結果
* @param token 刷新成功的token
*/
export function processQueue(error: any | null, token: string | null = null) {
// 遍歷所有隊列的接口
// 如果刷新成功則注入新token,否則直接reject錯誤信息
failedQueue.forEach(promise => {
if (error) {
// 刷新token失敗,直接reject錯誤信息
promise.reject(error);
} else if (token) {
// 調用resolve回調
promise.resolve(token);
}
});
// 最後清空隊列
failedQueue = [];
}
/**
* 清空隊列
*/
export function resetQueue() {
failedQueue = [];
}
這裏的核心主要是添加隊列和請求隊列接口的邏輯
addRequestToQueue函數為添加隊列邏輯,failedQueue其實是push了一個對象進去,該對象包含了請求邏輯
{
resolve: async (newToken: string) => {/* 重試邏輯 */},
reject: (err: any) => reject(err)
}
resolve函數:接收新token,更新請求頭並重試接口
reject函數:直接傳遞錯誤信息
而且函數內部的resolve(res)和reject(err)並非創建新的Promise,而是引用外層Promise的狀態控制器
這種設計實現了請求狀態的延遲控制:
- 當token過期時,先將請求邏輯存入隊列
- 等待token刷新完成後,通過調用隊列中對象的resolve/reject方法
- 最終決定外層Promise的狀態(成功/失敗)
調用processQueue函數,根據刷新token的狀態判斷調用隊列裏成功或失敗的回調。
request.ts——封裝axios的基本文件
import axios from "axios";
const service = axios.create({
baseURL: ""
});
// 請求攔截器
service.interceptors.request.use(
function (config: any) {
console.log("請求信息",config)
return config;
},
function (error: any) {
console.log("請求報錯", error);
return Promise.reject(error);
}
);
// 響應攔截器
service.interceptors.response.use(
async function (response: any) {
console.log("響應成功", response);
return Promise.resolve(error);
},
function (error: any) {
console.log("響應報錯", error);
return Promise.reject(error);
}
);
export default service;
request.ts——修改axios基本文件,加入雙token刷新機制流程
import axios from "axios";
import { getAccessToken, getRefreshToken, clearToken } from "@/views/test/auth";
import { refreshToken, addRequestToQueue, processQueue, resetQueue } from "@/views/test/refreshToken";
const service = axios.create({
baseURL: ""
});
// 是否刷新token中
let isRefreshing = false;
// 請求攔截器
service.interceptors.request.use(
function (config: any) {
// 是否為刷新token請求
if (config._isRefreshToken) {
// 為刷新token請求,則用refreshToken做請求頭
let refreshToken = getRefreshToken();
if (refreshToken) {
config.headers["Authorization"] = "Bearer " + refreshToken;
}
} else {
// 否則用accessToken做請求頭
let accessToken = getAccessToken();
if (accessToken) {
config.headers["Authorization"] = "Bearer " + accessToken;
}
}
return config;
},
function (error: any) {
console.log("請求報錯", error);
return Promise.reject(error);
}
);
// 響應攔截器
service.interceptors.response.use(
async function (response: any) {
// 1、將成功的請求放到最開始判斷
if (response.data.code === 200) {
return Promise.resolve(response.data);
}
// 2、刷新token的請求也報錯了,説明refreshToken也不可用
if (response.data.code !== 200 && response.config._isRefreshToken) {
console.log("刷新token也過期了");
clearToken();
resetQueue();
// 可以做其它邏輯處理,比如登錄...
// 省略其它...
return Promise.reject(response.data);
}
// 3、刷新token中
if (isRefreshing) {
// 刷新token過程中新進來的請求全部放隊列中等待
return addRequestToQueue(response);
}
// 4、正常請求的接口進來token過期,並且不是刷新token的請求,而且也沒有在請求刷新token中
if (response.data.code === 401 && !response.config._isRefreshToken && !isRefreshing) {
try {
// 開始刷新Token
isRefreshing = true;
// 刷新token,這裏請求刷新接口,又發了一次請求,所以這個請求必須Promise.resolve()返回,否者後面的邏輯不會執行
await refreshToken();
// 最初過期接口的重新請求
response.config.headers["Authorization"] = "Bearer " + getAccessToken();
const originalResponse = await service.request(response.config);
// 處理隊列中的請求
processQueue(null, getAccessToken());
// 返回請求結果
return originalResponse;
} catch (refreshError) {
// 刷新失敗處理
console.error("刷新Token失敗:", refreshError);
// 隊列裏的請求全部拋錯
processQueue(refreshError);
// 清除Token
clearToken();
// 其他邏輯,比如返回登錄頁...
// ...
return Promise.reject(refreshError);
} finally {
console.log("刷新結束");
// 刷新完成
isRefreshing = false;
}
}
// 5、業務代碼錯誤處理
console.log("請求失敗,其它code碼處理");
return Promise.reject(response.data);
},
function (error: any) {
console.log("響應報錯", error);
return Promise.reject(error);
}
);
export default service;
1、這段邏輯中,成功的接口先行,因為刷新token的接口也是一個新請求,如果不這樣做,刷新token的結果會被if (isRefreshing) {return addRequestToQueue(response);}截胡,導致sRefreshing狀態無法變更。
2、如果刷新token的接口也token過期,那就證明當前無可用token,可以返回到登錄頁。
3、刷新token中,新進來的接口全部進入隊列緩存,不返回結果,頁面的請求就會暫時等待,知道隊列緩存的接口resolve或reject。
4、這段邏輯就是刷新token和替換token的邏輯了,這裏面需要注意一點,最初的token過期接口需要優先返回,隊列的接口其次順序返回。
如果刷新token的接口報錯,則隊列裏的接口全部拋錯,之後的邏輯可以根據業務需求編寫,比如清除token、返回登錄頁。
最後就是其它狀態碼的邏輯處理,根據實際業務編寫。
頁面文件以及mock的接口
最後提供一下頁面文件和mock的接口,可以方便調試
<template>
<div class="snow-page">
<div class="snow-inner">
<a-space>
<a-button type="primary" @click="getToken">拿token</a-button>
<a-button type="primary" status="danger" @click="onTokenPast">token過期</a-button>
<a-button type="primary" status="warning" @click="onTokenRefreshPast">刷新token也過期</a-button>
<a-button type="outline" @click="onTokenRequest">其它Atoken接口請求</a-button>
</a-space>
</div>
</div>
</template>
<script setup lang="ts">
import { getTestTokenAPI, getTestTokenRefreshAPI, getTestRequestAPI } from "@/views/test/api";
import { setToken, getAccessToken, getRefreshToken } from "@/views/test/auth";
const getToken = async () => {
let res = await getTestTokenAPI();
let params = {
accessToken: res.data.accessToken,
refreshToken: res.data.refreshToken
};
setToken(params);
console.log("token拿到", res, getAccessToken(), getRefreshToken());
};
const onTokenPast = async () => {
let params = {
accessToken: "unknown",
refreshToken: "refreshToken-2"
};
setToken(params);
};
// 刷新token也過期
const onTokenRefreshPast = async () => {
let res = await getTestTokenRefreshAPI({ refreshToken: getRefreshToken(), past: true });
console.log("獲取過期刷新token數據", res);
};
let index = 100;
const onTokenRequest = async () => {
index++;
let res = await getTestRequestAPI({ index });
console.log("其它接口請求", res);
};
</script>
API接口
import Mock from "mockjs";
import type { MockMethod } from "vite-plugin-mock";
/** 返回成功數據 */
export const resultSuccess = (data: unknown) => {
return Mock.mock({
code: 200,
data,
message: "請求成功",
success: true
});
};
/** 返回失敗數據 */
export const resultError = (data: unknown, message: string, code = 500) => {
return Mock.mock({
code,
data,
message,
success: false
});
};
export default [
{
url: "/mock/test/getTestToken",
method: "get",
timeout: 300,
response: () => {
let params = {
accessToken: "accessToken-1",
refreshToken: "refreshToken-2"
};
return resultSuccess(params);
}
},
{
url: "/mock/test/getTestTokenRefresh",
method: "post",
timeout: 3000, // 延遲返回時間
response: ({ body }: any) => {
let { refreshToken, past } = body;
if (past) {
return resultError(null, "刷新token也過期了", 401);
} else {
let params = {
accessToken: "accessToken-3",
refreshToken: "refreshToken-4"
};
return resultSuccess(params);
}
}
},
{
url: "/mock/test/getTestRequest",
method: "get",
timeout: 300,
response: ({ headers, query }: any) => {
if (headers.authorization === "Bearer unknown") {
return resultError(null, "token失效", 401);
} else {
let params = { text: "其它請求成功", index: query.index };
return resultSuccess(params);
}
}
}
] as MockMethod[];