用 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,一经查实,立即删除!

相关文章

欢乐打地鼠小游戏html源码

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

kopf,一个实用的 Python 库!

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

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

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

【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…

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

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

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;提供专业的数…

【每日算法】

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

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

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

基于STM32F030设计的多点温度采集系统(BC26+OneNet)

一、项目背景 随着物联网技术的迅猛发展&#xff0c;越来越多的智能设备应运而生&#xff0c;而温度采集系统是其中重要的一类。在现代工业和家庭生活中&#xff0c;温度对于生产、居住和储存等过程的控制有着非常重要的作用。因此&#xff0c;准确地采集环境温度数据并进行处…

HTML做成一个粒子漩涡特效页面

大家好&#xff0c;今天制作制作一个粒子漩涡特效的页面&#xff01; 先看具体效果&#xff1a; 要在一个单一的 index.html 页面中实现粒子漩涡特效&#xff0c;我们可以使用HTML、CSS和JavaScript&#xff08;不需要外部库&#xff09;。下面是一个简单的例子&#xff0c;展…

JWT 从入门到精通

什么是 JWT JSON Web Token&#xff08;JWT&#xff09;是目前最流行的跨域身份验证解决方案 JSON Web Token Introduction - jwt.ioLearn about JSON Web Tokens, what are they, how they work, when and why you should use them.https://jwt.io/introduction 一、常见会…

Git发布正式

一般我们开发都是在测试环境开发&#xff0c;开发完成后再发布到正式环境。 一.分支代码合并到主分支1.首先切换到自己的分支(比如分支叫&#xff1a;dev)git checkout dev2.把本地分支拉取下来git pull 或者 git pull origin dev3.切换到主分支mastergit checkout master4.更新…

【Vue】购物车案例-构建项目

脚手架新建项目 (注意&#xff1a;勾选vuex) 版本说明&#xff1a; vue2 vue-router3 vuex3 vue3 vue-router4 vuex4/pinia vue create vue-cart-demo需要勾选上vuex&#xff0c;由于这个项目只有一个页面&#xff0c;vuex可勾可不勾 将原本src内容清空&#xff0c;替换成教学…

【计算机网络基础】IP地址

文章目录 一、IP介绍IP地址和Mac地址IP地址分类 二、IPV4地址IPV4地址分类子网掩码进制转换方法8421法则转换法私网地址PNAT技术IP分配原则 三、IPv6地址IPV6组成IPV6分类IPV6特殊地址 四、VLSM可变长子网掩码划分子网VLSM优点 &#x1f308;你好呀&#xff01;我是 山顶风景独…

springboot+mqtt使用总结

1.软件的选型 1.1.使用免费版EMQX 1.1.1.下载 百度搜索的目前是会打开官网&#xff0c;这里提供下免费版的使用链接EMQX使用手册 文档很详细&#xff0c;这里不再记录了。 1.2.使用rabbitmq rabbitmq一般做消息队列用&#xff0c;作为mqtt用我没有找到详细资料&#xff0c…