本文详细讲解如何在Vue3项目中利用Axios实现无感知的双Token(Access Token与Refresh Token)自动刷新机制。通过配置请求与响应拦截器,结合单例Promise队列处理并发过期请求,有效解决Token频繁失效导致的用户重复登录问题,提升系统安全性与用户体验。
一、引言
在现代Web系统中,基于Token的身份认证已成为保障用户数据安全的核心机制。然而,许多系统在Token刷新策略上存在缺陷,导致用户频繁遭遇登录失效,体验较差。随着安全策略对Token有效期的缩短,这种问题愈发突出。本文旨在结合Vue3与Axios技术栈,利用Promise异步机制,构建一套用户无感知的自动化Token刷新方案,从而在保障系统安全性的同时,显著提升应用的稳定性与用户交互体验。
二、示意图

三、具体实现
在明确基本流程后,实际编码工作看似简洁,但为确保Token刷新机制的稳健性与可靠性,仍需仔细处理多个关键细节。
- Token存储与管理:首要任务是确定Access Token与Refresh Token的安全存储方式。这涉及浏览器存储策略的选择,例如
localStorage或sessionStorage。本文不深入探讨存储策略的优劣,直接采用localStorage进行Token持久化存储。 - 请求拦截器配置:在Axios中配置请求拦截器,旨在每次发起HTTP请求前校验Token的有效性。若检测到Token已过期,则立即触发刷新流程。此环节需特别注意防止因并发请求导致的重复刷新问题。
- Token刷新响应逻辑:当Access Token过期时,系统需使用Refresh Token向服务器请求新的Access Token。此外,还需妥善处理刷新失败的场景,例如当Refresh Token也失效时,应引导用户重新进行登录操作。
- 引入队列机制:在Token刷新期间,可能有多个前端请求同时发出。为避免重复发起刷新请求,需引入队列机制,使其他请求在刷新完成前处于挂起状态,待新Token获取后再继续执行。
- 错误处理与用户体验:最后,需完善全流程的错误处理逻辑,包括刷新失败后的重试策略及错误提示,确保即使发生异常,用户感知到的体验影响降至最低。
通过上述步骤的实施,可构建出一个对用户透明、稳定可靠的双Token刷新机制,从而增强应用的安全防护能力并优化用户体验。下文将逐一解析这些关键步骤的具体代码实现。
1. 编写请求拦截器
实现请求拦截器的基础逻辑较为直观,即在每次请求时自动附加Token以完成身份认证。
service.interceptors.request.use((config: InternalAxiosRequestConfig) => {
const userStore = useUserStore()
if (userStore.authInfo.accessToken && userStore.authInfo.accessToken !== "") {
// 设置头部 token
config.headers.Authorization = RequestConstant.Header.AuthorizationPrefix + userStore.authInfo.accessToken;
}
return config;
}, (error: any) => {
return Promise.reject(error);
}
);
当前方案在请求包含有效Token时,会将其附加至请求头发送至服务器。但在某些特定场景下,部分接口可能无需携带Token。为此,可在请求配置中通过config对象来判断是否需携带Token。示例如下:
request: (deptId: number, deptForm: DeptForm): AxiosPromise<void> => {
return request<void>({
url: DeptAPI.UPDATE.endpoint(deptId),
method: "put",
data: deptForm,
headers: {
// 根据需要添加Token,或者通过自定义逻辑决定是否包含Authorization字段
token: false
}
});
}
因此,在请求拦截器中需增加判断逻辑,以确认当前请求头中的Token是否必须携带。
// 代码省略
2. 深究响应拦截器
双Token刷新机制的难点在于响应拦截器的处理,因为后端会在特定情况下返回Token过期的信息。我们需要首先明确后端接口的响应数据结构。
2.1 接口介绍
- 正常接口响应内容
// Status Code: 200 OK
{
"code":"0000",
"msg":"操作成功",
"data":{}
}
- accessToken 过期响应内容
// Status Code: 401 Unauthorized
{
"code":"I009",
"msg":"登录令牌过期"
}
- accessToken 刷新响应内容
// Status Code: 200 OK
{
"code": "0000",
"msg": "操作成功",
"data": {
"accessToken": "",
"refreshToken": "",
"expires": ""
}
}
- refreshToken 过期响应内容
// Status Code: 200 OK
{
"code": "I009",
"msg": "登录令牌过期"
}
注意:当Status Code不为200时,Axios的响应拦截器会自动进入error方法。在此处,我们可以捕获HTTP状态码为401的请求,初步判断请求因Unauthorized(未授权)引发。然而,触发401状态码的原因多样,并不一定代表Token过期。因此,为准确判断Token是否真正过期,需进一步检查响应体中的code字段。
2.2 响应拦截器编写
基于上述接口定义,编写逻辑相对简单:判断error.response?.status === 401且code === I009,若满足条件则直接触发Token刷新。
service.interceptors.response.use(async (response: AxiosResponse) => {
// 正常请求代码忽略
return Promise.reject(new Error(msg || "Error"));
},
async (error: any) => {
const userStore = useUserStore()
if (error.response?.status === 401) {
if (error.response?.data?.code === RequestConstant.Code.AUTH_TOKEN_EXPIRED) {
// token 过期处理
// 1. 刷新 token
const loginResult: LoginResult = await userStore.refreshToken()
if (loginResult) {
// refreshToken 未过期
// 2.1 重构请求头
error.config.headers.Authorization = RequestConstant.Header.AuthorizationPrefix + userStore.authInfo.accessToken;
// 2.2 请求
return await service.request(error.config);
} else {
// refreshToken 过期
// 1. 重置登录 token , 跳转登录页
await userStore.resetToken()
}
} else {
// 如果是系统发出的401 , 重置登录 token , 跳转登录页
await userStore.resetToken()
}
} else if (error.response?.status === 403) {
// 403 结果处理 , 代码省略
} else {
// 其他错误结果处理 , 代码省略
}
return Promise.reject(error.message);
}
);
2.3 解决重复刷新问题
完成上述逻辑后,需考虑多个请求同时遭遇Token过期的情况。若缺乏适当控制机制,这些请求可能同时发起刷新操作,导致重复请求,甚至触发后端安全机制将其标记为危险操作。
为解决此问题,我们实现了一个单例Promise的刷新逻辑,通过singletonRefreshToken变量确保同一时间仅有一个请求发起Token刷新操作。其核心思想是让所有需刷新的请求共享同一个Promise,即使多个请求同时遇到Token过期,它们也仅等待同一刷新操作的结果,从而避免多次刷新。
/**
* 刷新 token
*/
refreshToken(): Promise<LoginResult> {
// 如果 singletonRefreshToken 不为 null 说明已经在刷新中,直接返回
if (singletonRefreshToken !== null) {
return singletonRefreshToken
}
// 设置 singletonRefreshToken 为一个 Promise 对象 , 处理刷新 token 请求
singletonRefreshToken = new Promise<LoginResult>(async (resolve) => {
await AuthAPI.REFRESH.request({
accessToken: this.authInfo.accessToken as string,
refreshToken: this.authInfo.refreshToken as string
}).then(({data}) => {
// 设置刷新后的Token
this.authInfo = data
// 刷新路由
resolve(data)
}).catch(() => {
this.resetToken()
})
})
// 最终将 singletonRefreshToken 设置为 null, 防止 singletonRefreshToken 一直占用
singletonRefreshToken.finally(() => {
singletonRefreshToken = null;
})
return singletonRefreshToken
}
重要点解析:
singletonRefreshToken的使用:singletonRefreshToken是一个全局变量,用于保存当前正在进行的刷新操作。若某请求发现singletonRefreshToken不为null,则表明另一请求已发起刷新,该请求只需等待此操作完成,无需自行发起新刷新请求。
共享同一个
Promise:- 当
singletonRefreshToken被赋值为新的Promise时,所有遭遇Token过期的请求均返回此Promise并等待其结果。此举有效避免了同时发起多个刷新请求。
- 当
刷新完成后的处理:
- 刷新操作完成后(无论成功与否),均通过
finally将singletonRefreshToken置为null,从而确保下一次Token过期时能重新发起刷新请求。
- 刷新操作完成后(无论成功与否),均通过
通过该机制,可有效避免Token重复刷新问题,同时防止因过多重复请求引发的后端安全风险。该方法不仅提升了系统稳定性,还优化了资源使用,确保用户请求得到正确处理。
四、测试
- 当我们携带过期Token访问接口时,后端将返回401状态码及I009错误码。

此时进入响应拦截器的错误处理分支。
const loginResult: LoginResult = await userStore.refreshToken()
- 携带之前过期的accessToken和未过期的refreshToken进行刷新。
- 携带过期的accessToken的原因 :
- 防止未过期的 accessToken 进行刷新
- 防止 accessToken 和 refreshToken 不是同一用户发出的
- 其他安全性考虑

- 获取到正常结果

五、源码
前端源码位置 : yf/ yf-vue-admin / src / utils / request.ts
后端源码位置 : yf/ .. / impl / AuthServiceImpl.java
以上就是利用Axios实现无感知双Token刷新的详细教程的详细内容,更多关于Axios无感知Token刷新的资料请关注本站其它相关文章!