后臺管理系統登錄模塊(雙token的實現思路)

最近在寫后臺管理,這里分享一下我的登錄模塊的實現,我是使用react+typescript實現的,主要是登錄的邏輯和雙token的處理方式,請求接口的二次封裝aixos

1.首先我們需要渲染登錄界面的窗口,這個很簡單就不詳細講解了,然后主要就是關于點擊登錄按鈕的接口的調用

封裝我們的接口(封裝是非常有必要的):

下面是我們的登錄接口,然后request就是我二次封裝的axios

export function login(data: LoginData) {return request.post<LoginResult>('/backstage/login', data);
}

這里詳細講解一下關于axios的封裝,對于每一個要寫項目的時候,只要有后端請求接口,我們都需要封裝axios,這一步很重要,下面是對axios封裝的主要實現,主要是基于雙tokenaxios二次封裝請求

創建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;

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/web/89764.shtml
繁體地址,請注明出處:http://hk.pswp.cn/web/89764.shtml
英文地址,請注明出處:http://en.pswp.cn/web/89764.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

第十四講 | AVL樹實現

AVL樹實現一、AVL的概念二、AVL樹的實現1、AVL樹的結構2、AVL樹的插入&#xff08;1&#xff09;、AVL樹插入一個值的大概過程&#xff08;2&#xff09;、平衡因子更新更新原則更新停止條件插入結點及更新平衡因子的代碼實現3、旋轉&#xff08;1&#xff09;、旋轉的原則&…

《P3398 倉鼠找 sugar》

題目描述小倉鼠的和他的基&#xff08;mei&#xff09;友&#xff08;zi&#xff09;sugar 住在地下洞穴中&#xff0c;每個節點的編號為 1~n。地下洞穴是一個樹形結構。這一天小倉鼠打算從從他的臥室&#xff08;a&#xff09;到餐廳&#xff08;b&#xff09;&#xff0c;而…

錘子助手插件功能六:啟用攔截消息撤回

錘子助手插件功能六&#xff1a;啟用攔截消息撤回錘子助手插件功能六&#xff1a;啟用攔截消息撤回&#x1f6e1;? 插件簡介 攔截撤回消息&#xff0c;信息不再消失&#x1f527; 功能說明?? 使用風險與注意事項&#x1f3af; 適合人群?? 結語錘子助手插件功能六&#xf…

深度解析:基于EasyX的C++黑白棋AI實現 | 算法核心+圖形化實戰

摘要 本文詳解C黑白棋AI實現&#xff0c;使用EasyX圖形庫打造完整人機對戰系統。涵蓋&#xff1a; 遞歸搜索算法&#xff08;動態規劃優化&#xff09; 棋盤狀態評估函數設計 圖形界面與音效集成 勝負判定與用戶交互 附完整可運行代碼資源文件&#xff0c;提供AI難度調節方案…

樹同構(Tree Isomorphism)

樹同構&#xff08;Tree Isomorphism&#xff09;?? 是圖論中的一個經典問題&#xff0c;主要研究兩棵樹在結構上是否“相同”或“等價”&#xff0c;即是否存在一種節點的一一對應關系&#xff0c;使得兩棵樹的結構完全一致&#xff08;不考慮節點的具體標簽或位置&#xff…

分享如何在保證畫質的前提下縮小視頻體積實用方案

大文件在通過互聯網分享或上傳時會遇到很多限制&#xff0c;比如電子郵件附件大小限制、社交媒體平臺的文件大小要求等。壓縮后的視頻文件更小&#xff0c;更容易上傳到網絡、發送給他人或共享在社交平臺上。它是一款無需安裝的視頻壓縮工具&#xff0c;解壓后直接運行&#xf…

SpringBoot 統一功能處理(攔截器、@ControllerAdvice、Spring AOP)

文章目錄攔截器快速入門攔截器詳解攔截路徑攔截器執行流程全局控制器增強機制(ControllerAdvice)統一數據返回格式&#xff08;ControllerAdvice ResponseBodyAdvice&#xff09;??全局異常處理機制??&#xff08;ControllerAdvice ExceptionHandler&#xff09;全局數據…

建筑墻壁損傷缺陷分割數據集labelme格式7820張20類別

數據集格式&#xff1a;labelme格式(不包含mask文件&#xff0c;僅僅包含jpg圖片和對應的json文件)圖片數量(jpg文件個數)&#xff1a;7820標注數量(json文件個數)&#xff1a;7820標注類別數&#xff1a;20標注類別名稱:["Graffiti","Bearing","Wets…

圖書管理軟件iOS(iPhone)

圖書管理軟件iOS(iPhone)開發進度表2025/07/19圖書管理軟件開發開始一&#xff1a;圖書管理軟件開發iOS&#xff08;iPhone&#xff09;

MySQL配置性能優化

技術文章大綱&#xff1a;MySQL配置性能優化賽 引言 介紹MySQL性能優化的重要性&#xff0c;特別是在高并發、大數據場景下的挑戰。概述MySQL配置優化的核心方向&#xff08;如內存、查詢、索引等&#xff09;。引出比賽目標&#xff1a;通過配置調整提升MySQL性能指標&#xf…

uniapp微信小程序 實現swiper與按鈕實現上下聯動

1. 需求&#xff1a;頁面頂部展示n個小圖標。當選中某個圖標時&#xff0c;下方視圖會相應切換&#xff1b;反之&#xff0c;當滑動下方視圖時&#xff0c;頂部選中的圖標也會同步更新。 2. 思路&#xff1a; 上方scroll-view 區域渲染圖標&#xff0c;并且可左右滑動&#xff…

44.sentinel授權規則

授權規則是對請求者的身份做一個判斷,有沒有權限來訪問。 需求:一般網關負責請求的轉發到微服務,可以做身份判斷。但是如果具體某個微服務的訪問地址直接透露給了外部,不是經過網關訪問過來的。那這種就沒有經過網關也就無法進行身份判斷了。這時候就需要sentinel的授權規…

[硬件電路-55]:絕緣柵雙極型晶體管(IGBT)的原理與應用

一、IGBT的原理&#xff1a;MOSFET與BJT的復合創新IGBT&#xff08;Insulated Gate Bipolar Transistor&#xff09;是一種復合全控型電壓驅動式功率半導體器件&#xff0c;其核心設計融合了MOSFET&#xff08;金屬氧化物半導體場效應晶體管&#xff09;的高輸入阻抗&#xff0…

取消office word中的段落箭頭標記

對于一個習慣用WPS的人來說&#xff0c;office word中的段落箭頭讓人非常難受&#xff0c;所以想要取消該功能點擊文件-更多-選項然后在顯示界面&#xff0c;找到段落標記&#xff0c;取消勾選即可最終效果

Win11 上使用 Qume 搭建銀河麒麟V10 arm版虛擬機

安裝全程需要下載3個文件&#xff0c;可在提前根據文章1.1、2.1、2.2網址下載。 1 QEMU軟件簡介與安裝流程 QEMU&#xff08;Quick Emulator&#xff09;是一個開源軟件&#xff0c;可以模擬不同的計算機硬件行為&#xff08;如模擬arm架構&#xff09;&#xff0c;并可以創建…

[Linux]進程 / PID

一、認識進程 --- PCB寫一個死循環程序執行起來&#xff0c;觀察進程ps ajx 顯示所有進程用分號可以在命令行的一行中執行多條指令&#xff0c;也可以用 && &#xff1a;ps ajx | head -1 && ps ajx | grep proc終止掉進程后再查看&#xff1a;所以 ./p…

【人工智能99問】門控循環但單元(GRU)的結構和原理是什么?(13/99)

文章目錄GRU&#xff08;Gated Recurrent Unit&#xff09;的結構與原理一、GRU的結構與原理1. 核心組件2. 計算原理&#xff08;數學公式&#xff09;二、GRU的使用場景三、GRU的優缺點優點&#xff1a;缺點&#xff1a;四、GRU的訓練技巧五、GRU的關鍵改進六、GRU的相關知識與…

去中心化協作智能生態系統

摘要&#xff1a; 本報告深入HarmonyNet系統的工程實現細節&#xff0c;從開發者視角出發&#xff0c;提供了模塊化的組件規范、基于API的數據交互協議、可直接執行的業務邏輯流程以及經過優化的、可渲染的系統圖表。報告的核心在于將V2.0的高層架構轉化為具體的模塊接口&#…

FPGA自學——整體設計思路

FPGA自學——整體設計思路 1.設計定義 寫一套硬件描述語言&#xff0c;能夠在指定的硬件平臺上實現響應的功能 根據想要實現的功能進行設定&#xff08;如&#xff1a;讓LED一秒閃爍一次&#xff09; 2.設計輸入 方法&#xff1a; 編寫邏輯&#xff1a;使用verilog代碼描述邏輯…

ubuntu下好用的錄屏軟件

? 以下是 vokoscreen 的安裝教程,適用于 Linux 系統。vokoscreen 是一款簡單易用的屏幕錄制工具,支持錄制屏幕、攝像頭和音頻。 安裝 vokoscreen vokoscreen 提供了多種安裝方式,包括通過包管理器、Deb 包或 AppImage 文件。 方法 1:通過 apt 安裝(Ubuntu/Debian) su…