后台管理系统登录模块(双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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈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…