用 Axios 封装一个双 token 无感刷新

为什么要用双Token无感刷新,它解决了什么问题?

        为了保证安全性,后端设置的Token不可能长期有效,过了一段时间Token就会失效。而发送网络请求的过程又是需要携带Token的,一旦Token失效,用户就要重新登陆,这样用户可能需要频繁登录,体验不好。为了解决这个问题,采取双Token(Access_Token,Refresh_Token)无感刷新,用户完全体会不到Token的变化,但实际上,Token已经刷新了。

如何实现token的无感刷新。

在单点登录的环境下,服务器会给用户一个短时token,而另一个长时刷新token用于换取短时token。通过拦截器和配置更改,可以实现自动刷新token的功能。手动刷新token可以通过修改请求头中的token实现。同时,提到了一种通过excel创建基地址并使用拦截器来实现自动刷新token的方案。

单点登录的模式,并以C型加cookie模式为例详细讲解了用户如何完成登录流程。C型加cookie模式具有很强的控制力,但存在着烧钱、认证中心压力大等问题。为了降低成本和减轻认证中心的压力,出现了Token模式,用户登录后生成一个不能被篡改的字符串作为Token,而不再在服务器表格中记录任何东西。Token模式的优势在于成本低,但控制力较弱。

用户登录之后,会返回一个用户的标识,之后带上这个标识请求别的接口,就能识别出该用户。

标识登录状态的方案有两种:session jwt

session 是通过 cookie 返回一个 id,关联服务端内存里保存的 session 对象,请求时服务端取出 cookie 里 id 对应的 session 对象,就可以拿到用户信息。

jwt 不在服务端存储,会直接把用户信息放到 token 里返回,每次请求带上这个 token,服务端就能从中取出用户信息。 token 一般是放在一个叫 authorization 的 header 里。

双 token 验证流程
  1. 用户登录向服务端发送账号密码,登录失败返回客户端重新登录。登录成功服务端生成 accessToken 和 refreshToken,返回生成的 token 给客户端。
  2. 在请求拦截器中,请求头中携带 accessToken 请求数据,服务端验证 accessToken 是否过期。token 有效继续请求数据,token 失效返回失效信息到客户端。
  3. 客户端收到服务端发送的请求信息,在二次封装的 axios 的响应拦截器中判断是否有 accessToken 失效的信息,没有返回响应的数据。有失效的信息,就携带 refreshToken 请求新的 accessToken。
  4. 服务端验证 refreshToken 是否有效。有效,重新生成 token, 返回新的 token 和提示信息到客户端,无效,返回无效信息给客户端。
  5. 客户端响应拦截器判断响应信息是否有 refreshToken 有效无效。无效,退出当前登录。有效,重新存储新的 token,继续请求上一次请求的数据。

这两种方案一个服务端存储,通过 cookie 携带标识,一个在客户端存储,通过 header 携带标识。

session 的方案默认不支持分布式,因为是保存在一台服务器的内存的,另一台服务器没有

jwt 的方案天然支持分布式,因为信息保存在 token 里,只要从中取出来就行。

前端部分在axios封装时候加拦截器判断token是否过期,我这里跟别人写的最大的不同点是:我创建了两个axios对象,一个正常数据请求用(server),另一个专门刷新token用(serverRefreshToken),这样写的好处是省去了易错的判断逻辑

refreshToken.js

import request from './request';
import { getRefreshToken } from './token';// 定义一个全局或模块级的变量来存储当前的刷新token请求的Promise
let promise = null;// 异步函数,用于刷新token
async function refreshToken() {// 如果已经有一个刷新token的请求正在进行,直接返回该Promiseif (promise) {return promise;}// 创建一个新的Promisepromise = new Promise((resolve, reject) => {// 打印日志,表示正在刷新tokenconsole.log('正在刷新token');// 发起GET请求到'/refresh_token'路径,并携带Authorization头部request.get('/refresh_token', {headers: {// 使用getRefreshToken函数获取的当前刷新tokenAuthorization: `Bearer ${getRefreshToken()}`,},// 假设这不是一个标准的请求选项,如果确实需要这样的属性,请确保你的请求库支持它// 否则,请移除或替换为正确的请求选项// __isRefreshToken: true, // 不规范的属性,通常不建议在请求配置中使用双下划线开头的属性}).then((resp) => {// 如果响应的code为0,表示请求成功,resolve Promise并返回trueif (resp.code === 0) {resolve(true);} else {// 如果code不为0,可能表示请求失败或token无效,resolve Promise并返回falseresolve(false);}}).catch((error) => {// 如果请求发生错误,reject Promise并传递错误对象reject(error);});});// 无论Promise是resolve还是reject,都会执行finally块中的代码promise.finally(() => {// 将promise设置为null,表示刷新token的请求已经完成promise = null;});// 返回Promise,允许调用者使用await等待其完成return promise;
}

request.js

import axios from 'axios'
import { refreshToken, isRefreshRequest } form './refreshToken.js'// 创建axios实例
const service = axios.create({baseURL: '',// 所有的请求地址前缀部分timeout: 25000, // 请求超时时间(毫秒)withCredentials: true// 异步请求携带cookie
})// 请求拦截器
service.interceptors.request.use((config: any) => {...
}, error => {...
})// 响应拦截器
service.interceptors.response.use((response: any) => {let res = response.dataif (res.code == '401' && !isRefreshRequest(res.config)){ // 如果没有权限且不是刷新token的请求// 刷新tokentry {const res = await refreshToken()// 保存新的tokenlocalStorage.setItem('token', res.data.token)// 有新token后再重新请求response.config.headers.token = localStorage.getItem('token') // 新tokenconst resp = await service.request(response.config)return resp.data// return service(response.config)}catch {localStorage.clear() // 清除tokenrouter.replace('/login') // 跳转到登录页}}
}, error => {...console.log('error', error)return Promise.reject(error)
})

问题一:如何防止多次刷新token

为了防止多次刷新token,可以通过一个变量isRefreshing 去控制是否在刷新token的状态

import axios from 'axios'
import { refreshToken, isRefreshRequest } form './refreshToken.js'// 创建axios实例
const service = axios.create({baseURL: '',// 所有的请求地址前缀部分timeout: 25000, // 请求超时时间(毫秒)withCredentials: true// 异步请求携带cookie
})// 请求拦截器
service.interceptors.request.use((config: any) => {...
}, error => {...
})// 响应拦截器
service.interceptors.response.use((response: any) => {let res = response.datalet isRefreshing = falseif (res.code == '401' && ! isRefreshRequest(res.config)){ // 如果没有权限且不是刷新token的请求if (!isRefreshing) {isRefreshing = true// 刷新tokentry {const res = await refreshToken()// 保存新的tokenlocalStorage.setItem('token', res.data.token)// 有新token后再重新请求response.config.headers.token = localStorage.getItem('token') // 新tokenconst resp = await service.request(response.config)return resp.data// return service(response.config)}catch {localStorage.clear() // 清除tokenrouter.replace('/login') // 跳转到登录页}isRefreshing = false}}
}, error => {...console.log('error', error)return Promise.reject(error)
})

问题二:同时发起两个或者两个以上的请求时,怎么刷新token

当第二个过期的请求进来,token正在刷新,我们先将这个请求存到一个数组队列中,想办法让这个请求处于等待中,一直等到刷新token后再逐个重试清空请求队列。

那么如何做到让这个请求处于等待中呢?

为了解决这个问题,我们得借助Promise。将请求存进队列中后,同时返回一个Promise,让这个Promise一直处于Pending状态(即不调用resolve),此时这个请求就会一直等啊等,只要我们不执行resolve,这个请求就会一直在等待。当刷新请求的接口返回来后,我们再调用resolve,逐个重试。

import axios from 'axios'
import { refreshToken, isRefreshRequest } form './refreshToken.js'// 创建axios实例
const service = axios.create({baseURL: '',// 所有的请求地址前缀部分timeout: 25000, // 请求超时时间(毫秒)withCredentials: true// 异步请求携带cookie
})// 请求拦截器
service.interceptors.request.use((config: any) => {...
}, error => {...
})// 响应拦截器
service.interceptors.response.use((response: any) => {let res = response.datalet isRefreshing = falselet requests = [] // 请求队列if (res.code == '401' && isRefreshRequest(res.config)){ // 如果没有权限且不是刷新token的请求if (!isRefreshing) {isRefreshing = true// 刷新tokentry {const res = await refreshToken()// 保存新的tokenlocalStorage.setItem('token', res.data.token)// 有新token后再重新请求response.config.headers.token = localStorage.getItem('token') // 新token// token 刷新后将数组的方法重新执行requests.forEach((cb) => cb(token))requests = [] // 重新请求完清空const resp = await service.request(response.config)return resp.data// return service(response.config)}catch {localStorage.clear() // 清除tokenrouter.replace('/login') // 跳转到登录页}isRefreshing = false} else {// 返回未执行 resolve 的 Promisereturn new Promise(resolve => {// 用函数形式将 resolve 存入,等待刷新后再执行request.push(token => {response.config.headers.token = `${token}`resolve(service(response.config))})})}}
}, error => {...console.log('error', error)return Promise.reject(error)
})

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/web/25417.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

前台怎么调用Oracle proc过程

前台调用Oracle PROC过程通常涉及几种不同的方法,具体取决于你的前台应用程序所使用的技术和框架。以下是一些常见的方法,以及如何在前台调用Oracle PROC过程的详细步骤: 1. 使用PL/SQL Developer或其他SQL工具 直接调用:在PL/S…

将操作与数据分离 - 面向数据编程 v1.1

面向数据编程 (DOP) 非常注重数据,此次讨论的原则涉及实现大多数域逻辑的方法,它建议将操作与数据分开。 1.示例场景 此次讨论面向数据编程 v1.1的将操作与数据分离原则的具体示例是以销售平台作为示例,该平台销售书籍、家具和电子设备&…

欢乐打地鼠小游戏html源码

这是一款简单的js欢乐打地鼠游戏,挺好玩的,老鼠出来用鼠标点击锤它,击中老鼠获得一积分。 欢乐打地鼠小游戏html源码

kopf,一个实用的 Python 库!

更多Python学习内容:ipengtao.com 大家好,今天为大家分享一个实用的 Python 库 - kopf。 Github地址:https://github.com/nolar/kopf 在 Kubernetes 中,Operator 是一种用于扩展 Kubernetes 功能的强大工具。Operator 可以自动化应…

MySQL之查询性能优化(十三)

查询性能优化 优化LIMIT分页 在系统中需要进行分页操作的时候,我们通常会使用LIMIT加上偏移量的办法实现,同时加上合适的ORDER BY子句。如果有对应的索引,通常效率会不错,否则,MySQL需要做大量的文件排序操作。一个非…

【Python】成功解决TypeError: ‘int’ object is not iterable

【Python】成功解决TypeError: ‘int’ object is not iterable 🌈 欢迎莅临我的个人主页👈这里是我深耕Python编程、机器学习和自然语言处理(NLP)领域,并乐于分享知识与经验的小天地!🎇 &#…

MySQL的group by与count(), *字段使用问题

文章目录 问题group by到底做了什么举个例子简单来说为什么select字段,count()不能和*共同使用总结 问题 这是一段摘抄自MySQL官网的文字。其大致意思是MySQL拓展了group by的使用,MySQL允许选择没有出现在group by中的字段。换句话说,标准SQ…

【Python】成功解决ZeroDivisionError: division by zero

【Python】成功解决ZeroDivisionError: division by zero 🌈 欢迎莅临我的个人主页👈这里是我深耕Python编程、机器学习和自然语言处理(NLP)领域,并乐于分享知识与经验的小天地!🎇 &#x1f393…

【QT5.14.2】编译MQTT库example的时候报No such file or directory

【QT5.14.2】编译MQTT库example的时候报No such file or directory 前几天导师让跑一下MQTT库,用的5.14.2版本的QT,于是就上网搜了一个教程:https://www.bilibili.com/video/BV1dH4y1e7hG/?spm_id_from333.337.search-card.all.click&v…

Fedora的远程桌面

要在 Fedora 40 上开启远程桌面功能。 首先,要确保已安装 gnome-remote-desktop 和 vino 包。 这些软件包通常默认安装在 Fedora 的 GNOME 桌面环境中。 可以按照以下步骤操作: 1、判断电脑是否安装了 gnome-remote-desktop 和 vino 包: tomfedora:…

第十三周 5.28 三个修饰符知识点

一、abstract[抽象的] 1.abstract可以修饰类: (1)被abstract修饰的类称为抽象类 (2) 语法:abstract class 类名{} (3) 特点:抽象类只能声明引用,不能创建对象 (4) 抽象类中可以定义属性和成员方法、构造方法 2.abstr…

SpringSecurity提供了哪些核心功能?

Spring Security 是一个强大且高度可定制的身份验证和访问控制框架,它是为保护基于Spring的应用程序而设计的。Spring Security 提供了下列核心功能: 1. 全面的身份验证支持 Spring Security 支持广泛的身份验证机制,包括表单基础认证、HTT…

【Linux】匿名管道的应用场景 --- 进程池

👦个人主页:Weraphael ✍🏻作者简介:目前正在学习c和算法 ✈️专栏:Linux 🐋 希望大家多多支持,咱一起进步!😁 如果文章有啥瑕疵,希望大佬指点一二 如果文章对…

Tomcat中轻松部署Java Web项目

Tomcat 是一个广泛使用的 Java Servlet 容器和 Web 服务器,它允许你部署 Java Web 应用程序。以下是使用 Tomcat 部署 Java 项目的基本步骤: 1. 准备 Java 项目 确保你的 Java 项目是一个 Web 应用程序,即它包含了一个 WEB-INF 目录&#x…

Qt qtpropertybrowser使用实例(1)

属性界面实例&#xff1a; 代码如下&#xff1a; #include <QDate> #include <QLocale> #include "qtpropertymanager.h" #include "qtvariantproperty.h" #include "qttreepropertybrowser.h" int main(int argc, char *argv[]) {…

nginx mirror流量镜像详细介绍以及实战示例

nginx mirror流量镜像详细介绍以及实战示例 1.nginx mirror作用2.nginx安装3.修改配置3.1.nginx.conf3.2.conf.d目录下添加default.conf配置文件3.3.nginx配置注意事项3.3.nginx重启 4.测试 1.nginx mirror作用 为了便于排查问题&#xff0c;可能希望线上的请求能够同步到测试…

TalkingData 是一家专注于提供数据统计和分析解决方案的独立第三方数据智能服务平台

TalkingData 是一家专注于提供数据统计和分析解决方案的独立第三方数据智能服务平台。通过搜索结果&#xff0c;我们可以了解到 TalkingData 的一些关键特性和市场情况&#xff0c;并将其与同类型产品进行比较。 TalkingData 产品特性 数据统计与分析&#xff1a;提供专业的数…

OSX-KVM - 在 QEMU/KVM上运行macOS

文章目录 依赖安装准备安装Headless macOSSetting Expectations Right安装后这合法吗&#xff1f;动机回馈贡献 OSX-KVM 支持早 QEMU/KVM上运行macOS。现在支持OpenCoreMontereyVenturaSonoma&#xff01; 现在仅提供商业&#xff08;付费&#xff09;支持&#xff0c;以避免垃…

【每日算法】

算法第15天| (二叉树part02)层序遍历、226.翻转二叉树(优先掌握递归)、101. 对称二叉树(优先掌握递归) 文章目录 算法第15天| (二叉树part02)层序遍历、226.翻转二叉树(优先掌握递归)、101. 对称二叉树(优先掌握递归)一、层序遍历二、226. 翻转二叉树(优先掌握递归)三、101. 对…

Elasticsearch index 设置 false,为什么还可以被检索到?

在 Elasticsearch 中&#xff0c;mapping 定义了索引中的字段类型及其处理方式。 近期有球友提问&#xff0c;为什么设置了 index: false 的字段仍能被检索。 本文将详细探讨这个问题&#xff0c;并引入列式存储的概念&#xff0c;帮助大家更好地理解 Elasticsearch 的存储和查…