最近在寫后臺管理,這里分享一下我的登錄模塊的實現,我是使用react+typescript實現的,主要是登錄的邏輯和雙token的處理方式,請求接口的二次封裝aixos
1.首先我們需要渲染登錄界面的窗口,這個很簡單就不詳細講解了,然后主要就是關于點擊登錄按鈕的接口的調用
封裝我們的接口(封裝是非常有必要的):
下面是我們的登錄接口,然后request就是我二次封裝的axios
export function login(data: LoginData) {return request.post<LoginResult>('/backstage/login', data);
}
這里詳細講解一下關于axios的封裝,對于每一個要寫項目的時候,只要有后端請求接口,我們都需要封裝axios,這一步很重要,下面是對axios封裝的主要實現,主要是基于雙token的axios二次封裝請求
創建axios的封裝核心文件(request)講解:
1.雙token的實現邏輯?
1.1長短token是什么:
當用戶登錄成功之后會返回一個json數據,里面有連個token,一個是短token,access_token,一個是長token,refresh_token
access_token是訪問令牌,因為在請求具有權限接口的時候需要請求頭,里面需要放用戶token,這個請求頭里面的token就是我們的access_token,access_token的存在時間很短
refresh_token是刷新令牌,用于生成短token
1.2雙token的更新更新邏輯:
當access_token過期時,需要使用refresh_token來生成一個新的access_token,那么什么時候會觸發這個刷新機制呢,其實就是當調用權限接口的時候,如果access_token過期了,服務器教會返回一個401?unauthorized,前端的響應攔截器會獲取所有的api請求,當它獲取到401的時候,就知道access_token過期了,然后就刷新token了
當獲取到了access_token之后,需要存儲access_token,這是為了確保后續所有新發起的 API 請求都能使用這個最新的access_token。之前失敗的請求并沒有被直接拋棄,而是被暫存到了一個“待重試隊列”(requestsToRetry)中。重新發送新的請求
2.axios二次封裝實現思路:
實現引入我們的核心庫,第一行就不進行解釋了,第二行就是自己封裝的一下存儲,獲取,清除token的自己封裝的一些方法,見名知意
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse, AxiosError } from 'axios';
import { getAccessToken, getRefreshToken, setTokens, clearTokens } from './token';
2.1創建隊列和函數:
isRefreshing,用于確保在多個請求同時 401 時,只有一個 token刷新請求被發送,防止重復刷新。
failedQueue隊列:當isRefreshing為true時(表示已經有刷新 token 的請求在進行中),所有后續因 401 失敗的請求都會被推入failedQueue。這些請求會返回一個新的 Promise,等待 token?刷新成功后被解決。
processQueue?函數:負責在 token?刷新成功或失敗后,處理 failedQueue?中的所有請求。如果成功,用新的 token?重新發送;如果失敗,則拒絕這些請求。
let isRefreshing: boolean = false;
// 存儲因 token 過期而失敗的請求隊列
// 定義隊列中每個元素的類型
interface FailedRequest {resolve: (value?: string | PromiseLike<string>) => void;reject: (reason?: any) => void;
}
let failedQueue: FailedRequest[] = []const processQueue = (error: Error | null, token: string | null = null): void => {failedQueue.forEach(prom => {if (error) {prom.reject(error);} else {prom.resolve(token as string); // 確保在沒有錯誤時 token 不為 null}});failedQueue = [];
};
2.2創建axios實例:
使用 axios.create()?創建一個獨立的 axios?實例,避免污染全局 axios
const request: AxiosInstance = axios.create({// 在 .env 文件中配置 中的請求配置baseURL: api基礎路徑,timeout: 10000, // 請求超時時間
});
2.3創建請求攔截器:
- 動態添加access_token,從存儲位置獲取access_token,并將其添加到請求頭 Authorization?中。通常格式為 Bearer ${token}。
- 除了上面的請求頭,還可以添加其他請求頭,如 Content-Type。
- 可以實現全局的請求 Loading 動畫。
// --- 請求攔截器 ---
request.interceptors.request.use((config: InternalAxiosRequestConfig) => {const accessToken = getAccessToken();if (accessToken) {// 在請求頭中添加 Authorization 字段if (!config.headers) {config.headers = new axios.AxiosHeaders();}config.headers['Authorization'] = `Bearer ${accessToken}`;}return config;},(error: AxiosError) => {return Promise.reject(error);},
);
2.4創建響應攔截器:
后端返回的數據通常會包裹在data當中,可以直接返回response.data
處理 HTTP 狀態碼非 2xx?的錯誤
無感刷新 token?(核心):
- 捕獲401 Unauthorized錯誤:這是最關鍵的一步。當后端因為access_token過期而返回401?時,攔截此錯誤。? ? ? ? ? ? ? ??
- 調用刷新接口:使用refresh_token去請求新的access_token。? ? ? ? ? ? ? ??
- 重發失敗請求:獲取到新的access_token后,將剛才失敗的請求(error.config)用新 token重新發送一次。? ? ? ? ? ? ??
- ?并發請求處理:當多個請求同時因為 token 過期而失敗時,要確保刷新 token的接口只被調用一次。后續失敗的請求應被“暫存”,等待新 token?獲取后再統一重發。
// --- 響應攔截器 ---
request.interceptors.response.use(// 響應成功 (HTTP 狀態碼為 2xx)(response: AxiosResponse<any>) => {// 通常后端會把數據包裹在 data 中,這里直接返回 data,簡化業務代碼return response.data;}, // 響應失敗 (HTTP 狀態碼非 2xx)async (error: AxiosError) => {const originalRequest = error.config as| (InternalAxiosRequestConfig & { _retry?: boolean })| undefined;// 如果沒有config,直接返回錯誤if (!originalRequest) {console.error('Request Error: No config available');return Promise.reject(error);} // 檢查是否是 401 Unauthorized 錯誤,并且不是刷新 token 的請求本身if (error.response?.status === 401 && !originalRequest._retry) {// 如果正在刷新 token,則將當前失敗的請求加入隊列if (isRefreshing) {return new Promise<string>((resolve, reject) => {failedQueue.push({resolve: (value?: string | PromiseLike<string>) => resolve(value as string),reject,});}).then((token) => {if (!originalRequest.headers) {originalRequest.headers = new axios.AxiosHeaders();}originalRequest.headers['Authorization'] = `Bearer ${token}`;return request(originalRequest); // 使用新 token 重新發送請求}).catch((err) => {return Promise.reject(err);});}originalRequest._retry = true; // 標記此請求已嘗試過重試isRefreshing = true;const refreshToken = getRefreshToken();if (!refreshToken) {// 如果沒有 refresh_token,直接跳轉到登錄頁console.error('No refresh token available.');clearTokens(); // window.location.href = '/login'; // 或使用 router.push('/login')return Promise.reject(new Error('No refresh token, redirect to login.'));}try {// --- 調用刷新 Token 的 API ---// 注意:這里需要使用一個不帶攔截器的 axios 實例來發請求,避免循環調const response = await axios.post<{data: {access_token: string;refresh_token: string;};}>('登錄接口api', {refresh_token: refreshToken,});const { access_token: newAccessToken, refresh_token: newRefreshToken } = response.data.data; // 1. 更新本地存儲的 tokensetTokens(newAccessToken, newRefreshToken); // 2. 處理并重發等待隊列中的請求processQueue(null, newAccessToken); // 3. 重發本次失敗的請求if (!originalRequest.headers) {originalRequest.headers = new axios.AxiosHeaders();}originalRequest.headers['Authorization'] = `Bearer ${newAccessToken}`;return request(originalRequest);} catch (refreshError: unknown) {// 刷新 token 失敗,清除所有 token 并重定向到登錄頁console.error('Failed to refresh token:', refreshError);clearTokens();processQueue(refreshError as Error, null); // window.location.href = '/login'; // 或使用 router.push('/login')return Promise.reject(refreshError);} finally {isRefreshing = false;}} // 對于其他錯誤,直接拋出// 處理錯誤信息,確保類型安全const errorMessage =error.response?.data &&typeof error.response.data === 'object' &&'message' in error.response.data? (error.response.data as { message: string }).message: error.message;console.error('Request Error:', errorMessage);return Promise.reject(error);},
);
?
2.5完整的axios二次封裝(request):
import axios, { AxiosInstance, InternalAxiosRequestConfig, AxiosResponse, AxiosError } from 'axios';
import { getAccessToken, getRefreshToken, setTokens, clearTokens } from '../stores/token';// --- 狀態變量 ---
// 標記是否正在刷新 token,防止重復刷新
let isRefreshing: boolean = false;
// 存儲因 token 過期而失敗的請求隊列
// 定義隊列中每個元素的類型
interface FailedRequest {resolve: (value?: string | PromiseLike<string>) => void;reject: (reason?: unknown) => void;
}
let failedQueue: FailedRequest[] = [];/*** @description 處理隊列中的請求* @param {Error | null} error - 刷新 token 過程中的錯誤* @param {string | null} token - 新的 access_token*/const processQueue = (error: Error | null, token: string | null = null): void => {failedQueue.forEach((prom) => {if (error) {prom.reject(error);} else {prom.resolve(token as string); // 確保在沒有錯誤時 token 不為 null}});failedQueue = [];
};// --- 創建 Axios 實例 ---
const request: AxiosInstance = axios.create({// 在 .env 文件中配置 中的請求配置baseURL: '基礎api',timeout: 10000, // 請求超時時間
});// --- 請求攔截器 ---
request.interceptors.request.use((config: InternalAxiosRequestConfig) => {const accessToken = getAccessToken();if (accessToken) {// 在請求頭中添加 Authorization 字段if (!config.headers) {config.headers = new axios.AxiosHeaders();}config.headers['Authorization'] = `Bearer ${accessToken}`;}return config;},(error: AxiosError) => {return Promise.reject(error);},
);// --- 響應攔截器 ---
request.interceptors.response.use(// 響應成功 (HTTP 狀態碼為 2xx)(response: AxiosResponse<any>) => {// 通常后端會把數據包裹在 data 中,這里直接返回 data,簡化業務代碼return response.data;}, // 響應失敗 (HTTP 狀態碼非 2xx)async (error: AxiosError) => {const originalRequest = error.config as| (InternalAxiosRequestConfig & { _retry?: boolean })| undefined;// 如果沒有config,直接返回錯誤if (!originalRequest) {console.error('Request Error: No config available');return Promise.reject(error);} // 檢查是否是 401 Unauthorized 錯誤,并且不是刷新 token 的請求本身if (error.response?.status === 401 && !originalRequest._retry) {// 如果正在刷新 token,則將當前失敗的請求加入隊列if (isRefreshing) {return new Promise<string>((resolve, reject) => {failedQueue.push({resolve: (value?: string | PromiseLike<string>) => resolve(value as string),reject,});}).then((token) => {if (!originalRequest.headers) {originalRequest.headers = new axios.AxiosHeaders();}originalRequest.headers['Authorization'] = `Bearer ${token}`;return request(originalRequest); // 使用新 token 重新發送請求}).catch((err) => {return Promise.reject(err);});}originalRequest._retry = true; // 標記此請求已嘗試過重試isRefreshing = true;const refreshToken = getRefreshToken();if (!refreshToken) {// 如果沒有 refresh_token,直接跳轉到登錄頁console.error('No refresh token available.');clearTokens(); // window.location.href = '/login'; // 或使用 router.push('/login')return Promise.reject(new Error('No refresh token, redirect to login.'));}try {// --- 調用刷新 Token 的 API ---// 注意:這里需要使用一個不帶攔截器的 axios 實例來發請求,避免循環調const response = await axios.post<{data: {access_token: string;refresh_token: string;};}>('login接口', {refresh_token: refreshToken,});const { access_token: newAccessToken, refresh_token: newRefreshToken } = response.data.data; // 1. 更新本地存儲的 tokensetTokens(newAccessToken, newRefreshToken); // 2. 處理并重發等待隊列中的請求processQueue(null, newAccessToken); // 3. 重發本次失敗的請求if (!originalRequest.headers) {originalRequest.headers = new axios.AxiosHeaders();}originalRequest.headers['Authorization'] = `Bearer ${newAccessToken}`;return request(originalRequest);} catch (refreshError: unknown) {// 刷新 token 失敗,清除所有 token 并重定向到登錄頁console.error('Failed to refresh token:', refreshError);clearTokens();processQueue(refreshError as Error, null); // window.location.href = '/login'; // 或使用 router.push('/login')return Promise.reject(refreshError);} finally {isRefreshing = false;}} // 對于其他錯誤,直接拋出// 處理錯誤信息,確保類型安全const errorMessage =error.response?.data &&typeof error.response.data === 'object' &&'message' in error.response.data? (error.response.data as { message: string }).message: error.message;console.error('Request Error:', errorMessage);return Promise.reject(error);},
);export default request;