利用Axios实现无感知双Token刷新的详细教程

 更新时间:2024年08月28日 08:51:23   作者:翼飞  
在现代系统中,Token认证已成为保障用户安全的标准做法,然而,尽管许多系统采用了这种认证方式,却在处理Token刷新方面存在不足,导致用户体验不佳,许多系统未能提供一种无缝的、用户无感知的Token刷新机制,所以本文介绍了教你用Axios实现无感知双Token刷新

一、引言

在现代系统中,Token认证已成为保障用户安全的标准做法。然而,尽管许多系统采用了这种认证方式,却在处理Token刷新方面存在不足,导致用户体验不佳。随着Token有效期的缩短,频繁的重新登录成为常见现象,许多系统未能提供一种无缝的、用户无感知的Token刷新机制。通过结合Vue3和Axios这两大前端技术栈,我们可以借助Promise机制,开发出一种更加完善的自动化Token刷新方案,显著提升系统的稳定性和用户体验。本文将深入探讨这一实现过程,帮助你解决Token刷新难题。

二、示意图

三、具体实现

了解了基本步骤后,实际的实现过程其实相当简洁。然而,在具体操作中,仍有许多关键细节需要我们仔细考量,以确保Token刷新机制的稳定性和可靠性。

  • Token 存储与管理:首先,明确如何安全地存储和管理Access Token与Refresh Token。这涉及到浏览器的存储策略,比如使用localStoragesessionStorage,存储策略不在本文中提及,本文采用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刷新的资料请关注脚本之家其它相关文章!

相关文章

  • js实现消灭星星(web简易版)

    js实现消灭星星(web简易版)

    这篇文章主要为大家详细介绍了js实现web简易版的消灭星星,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2020-03-03
  • 浅谈layui 数据表格前后台传值的问题

    浅谈layui 数据表格前后台传值的问题

    今天小编就为大家分享一篇浅谈layui 数据表格前后台传值的问题,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2019-09-09
  • 微信小程序开发WXML模板语法基础教程

    微信小程序开发WXML模板语法基础教程

    这篇文章主要介绍了微信小程序模板语法,WXML(WeiXin Markup Language)是框架设计的一套标签语言,结合基础组件、事件系统,可以构建出页面的结构,需要的朋友们下面随着小编来一起学习学习吧
    2022-08-08
  • JS 简单实现拖拽评星的示例代码

    JS 简单实现拖拽评星的示例代码

    本文主要介绍了JS 简单实现拖拽评星,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2023-05-05
  • js仿360开机效果

    js仿360开机效果

    这篇文章主要为大家详细介绍了js仿360开机效果,并且封装一个带回调函数的缓动动画,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2019-12-12
  • Javascript this指针

    Javascript this指针

    Javascript是一门基于对象的动态语言,也就是说,所有东西都是对象,一个很典型的例子就是函数也被视为普通的对象。
    2009-07-07
  • Bootstrap每天必学之按钮

    Bootstrap每天必学之按钮

    Bootstrap每天必学之按钮组工具栏,本文讲解的就是大家最为常用的按钮组工具栏,感兴趣的小伙伴们可以参考一下
    2015-11-11
  • Javascript实现视频轮播在pc端与移动端均可

    Javascript实现视频轮播在pc端与移动端均可

    用Javascript实现视频轮播,毕竟是客户的需求吗?所以尽量实现下,下面有个实现视频轮播的示例,pc端与移动端均可以实现,感兴趣的朋友可以了解下
    2013-09-09
  • WordPress中利用AJAX异步获取评论用户头像的方法

    WordPress中利用AJAX异步获取评论用户头像的方法

    这篇文章主要介绍了WordPress中利用AJAX异步获取评论用户头像的方法,文中的例子是输入邮箱即可获取头像,需要的朋友可以参考下
    2016-01-01
  • Bootstrap CSS组件之按钮下拉菜单

    Bootstrap CSS组件之按钮下拉菜单

    这篇文章主要为大家详细介绍了Bootstrap CSS组件之按钮下拉菜单,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2016-12-12

最新评论