一、引言
在现代系统中,token认证已成为保障用户安全的标准做法。然而,尽管许多系统采用了这种认证方式,却在处理token刷新方面存在不足,导致用户体验不佳。随着token有效期的缩短,频繁的重新登录成为常见现象,许多系统未能提供一种无缝的、用户无感知的token刷新机制。通过结合vue3和axios这两大前端技术栈,我们可以借助promise机制,开发出一种更加完善的自动化token刷新方案,显著提升系统的稳定性和用户体验。本文将深入探讨这一实现过程,帮助你解决token刷新难题。
二、示意图
三、具体实现
了解了基本步骤后,实际的实现过程其实相当简洁。然而,在具体操作中,仍有许多关键细节需要我们仔细考量,以确保token刷新机制的稳定性和可靠性。
- token 存储与管理:首先,明确如何安全地存储和管理access token与refresh token。这涉及到浏览器的存储策略,比如使用
localstorage
、sessionstorage
,存储策略不在本文中提及,本文采用localstorage 进行存储。 - 请求拦截器的设置:在axios中设置请求拦截器,用于在每次发送请求前检查token的有效性。如果发现token过期,则触发刷新流程。这一步骤需注意避免并发请求引发的重复刷新。
- 处理token刷新的响应逻辑:当token过期时,通过发送refresh token请求获取新的access token。在这里,需要处理刷新失败的情况,如refresh token也失效时,如何引导用户重新登录。
- 队列机制的引入:在token刷新过程中,可能会有多个请求被同时发出。为了避免重复刷新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 过期,如果没有适当的机制控制,这些请求可能会同时发起刷新 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刷新的资料请关注代码网其它相关文章!
发表评论