用 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.rhkb.cn/news/341961.html

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

相关文章

python书上的动物是啥

Python的创始人为Guido van Rossum。1989年圣诞节期间,在阿姆斯特丹,Guido为了打发圣诞节的无趣,决心开发一个新的脚本解释程序,做为ABC语言的一种继承。之所以选中Python作为程序的名字,是因为他是一个叫Monty Python…

C# 判断字符串不等于空的示例

在C#中,要判断一个字符串是否不等于空(即它既不是null也不是空字符串""),方法有如下几种,如下。 方法1 使用逻辑运算符和string.IsNullOrEmpty方法 string myString "123"; // 假设要检查的字…

重邮803计网概述

目录 一.计算机网络向用户提供的最重要的功能 二.互联网概述 1.网络的网络 2.计算机网络的概念 3. 互联网发展的三个阶段 4.制订互联网的正式标准要经过以下的四个阶段 5.互联网的组成(功能) 6.互联网功能 7.互联网的组成(物理&#…

java自学阶段二:JavaWeb开发50(Spring和Springboot学习)

Spring、Springboot基础知识学习 目录 学习目标Spring基础概念IOC控制反转DI依赖注入事务管理AOP面向切面编程Spring案例说明(Postman使用、Restful开发规范、lombok、Restful、nginx了解) 一:学习目标: 1)了解Sprin…

将小爱音箱接入 ChatGPT 和豆包ai改造成专属语音助手

这个GitHub项目,mi-gpt,旨在将小爱音箱和米家设备与ChatGPT和豆包集成,有效地将这些设备转变为个性化语音助手。以下是对其功能和设置的详细分析: 主要特点 角色扮演:该项目允许小爱适应不同的角色,如伴侣…

HCIA-WLAN实验-二层旁挂组网

目录 前言:拓扑说明创建新拓扑配置网络互通SW1上配置VLAN10 20 30SW1上放行对应的VLANSW2上创建vlan 10 20并在对应接口放行VLAN在AC上创建vlan10并放行对应接口在SW1上创建vlanif20和vlanif30,并配置对应的IP在AC上创建vlanif10并配置IP在路由器AR上配置…

形如SyntaxError: EOL while scanning string literal,以红色波浪线形式在Pycharm下出现

背景: 新手在学习Python时可能会出现如下图所示的报错 下面分情况教大家如何解决 视频教程【推荐】: 形如SyntaxError: EOL while scanning string literal,以红色波浪线形式在Pycharm下出现 过程: 问题概述: 简单…

[数据集][目标检测]道路圆石墩检测数据集VOC+YOLO格式461张1类别

数据集格式:Pascal VOC格式YOLO格式(不包含分割路径的txt文件,仅仅包含jpg图片以及对应的VOC格式xml文件和yolo格式txt文件) 图片数量(jpg文件个数):462 标注数量(xml文件个数):462 标注数量(txt文件个数):462 标注类别…

基于阿里云服务网格流量泳道的全链路流量管理(三):无侵入式的宽松模式泳道

作者:尹航 在前文《基于阿里云服务网格流量泳道的全链路流量管理(一):严格模式流量泳道》、《基于阿里云服务网格流量泳道的全链路流量管理(二):宽松模式流量泳道》中,我们介绍了流…

wpf工程中加入Hardcodet.NotifyIcon.Wpf生成托盘

1、在项目中用nuget引入Hardcodet.NotifyIcon.Wpf。如下图所示。 2、在App.xaml中创建托盘界面&#xff0c;代码是写在 App.xaml 里面 注意在application中一定要加入这一行代码&#xff1a; xmlns:tb"http://www.hardcodet.net/taskbar" 然后在<Application.R…

【免费Web系列】JavaWeb实战项目案例七(项目结束)

这是Web第一天的课程大家可以传送过去学习 http://t.csdnimg.cn/K547r 登录认证 在前面的课程中&#xff0c;我们已经实现了部门管理、员工管理的基本功能&#xff0c;但是大家会发现&#xff0c;我们并没有登录&#xff0c;就直接访问到了Tlias智能学习辅助系统的后台。 这…

韩国Neowine推出第三代强加密芯片ALPU-CV

推出第三代加密芯片&#xff1b;是ALPU系列中的高端IC&#xff1b;是一款高性能车规级加密芯片&#xff1b;其加密性更强、低耗电、体积小&#xff1b;使得防复制、防抄袭板子的加密性能大大提升&#xff0c;该芯片通过《AEC-Q100》认证&#xff0c;目前已经在国产前装车辆配件…

亚马逊测评自养号需要什么资源?

亚马逊测评自养号项目需要用到哪些资源呢&#xff1f; 1. 养号系统及软件 2. 代理IP资源 3. 收货地址及注册资料 4. 国外信用卡及礼品卡 5. 邮箱及手机号想做好这个项目以上的资源缺一不可 首先我们来说说养号的环境&#xff0c;养号的环境在以前的文章里也提到过&#x…

基于python的网上挂号预约系统-计算机毕业设计源码89352

摘 要 随着科学技术的飞速发展&#xff0c;社会的方方面面、各行各业都在努力与现代的先进技术接轨&#xff0c;通过科技手段来提高自身的优势&#xff0c;医院当然也不例外。网上挂号预约系统是以实际运用为开发背景&#xff0c;运用软件工程原理和开发方法&#xff0c;采用Py…

软件三班20240605

文章目录 1.创建工程和模块2.添加 web支持3.创建前端代码4.添加servlet 依赖5. 代码6.案例2 1.创建工程和模块 2.添加 web支持 方法1 方法2 3.创建前端代码 4.添加servlet 依赖 5. 代码 <!DOCTYPE html> <html lang"en"> <head><meta c…

【面试干货】SQL中count(*)、count(1)和count(column)的区别与用法

【面试干货】SQL中count&#xff08;*&#xff09;、count&#xff08;1&#xff09;和count&#xff08;column&#xff09;的区别与用法 1、count(*)2、count(1)3、count(column) &#x1f496;The Begin&#x1f496;点点关注&#xff0c;收藏不迷路&#x1f496; 在SQL中&a…

InnoDB存储引擎非常重要的一个机制--MVCC(多版本并发控制)

Mysql是如何实现隔离性的&#xff1f;&#xff08;锁MVCC&#xff09; 隔离性是指一个事务内部的操作以及操作的数据对正在进行的其他事务是隔离的&#xff0c;并发执行的各个事务之间不能相互干扰。隔离性可以防止多个事务并发执行时&#xff0c;可能存在交叉执行导致数据的不…

opencv进阶 ——(十一)基于RMBG实现生活照生成寸照

实现步骤 1、检测人脸&#xff0c;可以使用opencv自带的级联分类器或者dlib实现人脸检测 2、放大人脸范围&#xff0c;调整到正常寸照尺寸 3、基于RMGB算法得到人像掩码 4、生成尺寸相同的纯色背景与当前人像进行ALPHA融合即可 alpha融合实现 void alphaBlend(cv::Mat&…

c++(内存分配,构造,析构)

#include <iostream>using namespace std; class Per { private:string name;int age;double *height;double *weigh; public://无参构造Per(){cout << "Per::无参构造" << endl;}//有参构造Per(string name,int age,double height,double weigh):…

分布式锁redisson

1&#xff1a;pom.xml添加依赖 <dependency><groupId>org.redisson</groupId><artifactId>redisson-spring-boot-starter</artifactId><version>3.21.1</version> </dependency>2-1&#xff1a;方法一&#xff1a;读取默认ym…