Token令牌无感刷新
“无感刷新” 指的是在用户不感知的情况下,后台自动处理令牌刷新,并确保后续请求能够继续使用有效的令牌。为实现这种效果,通常会在令牌即将过期时提前刷新,以避免在实际请求时遇到 401 或其他认证失败的响应。
以下是无感刷新令牌的常见一般就两种实现思路:
请求提前根据揭露的过期时间提前做令牌刷新
优势:可以减少后端的请求次数,在第一个请求请求之前就可以判断是会否需要刷新令牌了,避免了不必要的网络开销,也能更好的的控制并发
劣势:如果电脑时间改动,前后端没有约定好当前系统时间处理的话,就会导致每次请求都在这刷新令牌
根据接口响应值做令牌刷新
优势:不需要顾忌电脑时间的改动,也不需要去考虑过期时间,直接以接口响应的状态来判断是否需要刷新令牌
劣势:刷新令牌依据接口响应,会多造成必要的网络开销,以及对大量请求的场景会造成接口响应积压的情况
由于两种方式,只是在处理令牌刷新的位置不同,其他需要注意的逻辑基本一样
比如:
刷新后更新请求:在令牌刷新后,自动更新后续所有请求的令牌。
并发请求的处理:如果多个请求同时触发令牌刷新,避免重复刷新。
以下axios
举例,以博主自己常用的方式 根据接口响应值做令牌刷新
介绍大概的用法
安装
请挑选适合自己的安装方式
npm install axios --save
yarn add axios
pnpm install axios
导入,声明
import axios from 'axios';
// 创建一个 Axios 实例
const api = axios.create({
baseURL: 'https://api.example.com',
timeout: 5000, // 请求超时时间
});
拦截器声明
博主个人习惯请求拦截和响应拦截单独写一个文件,看官们请根据自己喜好,爱咋搞就咋搞
创建request_interceptors.js
与response_interceptors.js
文件,并编写代码
request_interceptors.js
这个部分比较简单
为所有请求赋值请求令牌
为所有请求赋值请求时间
import { stores } from '@/stores';
export default {
success: function (config) {
config.headers['X-Real-Request'] = new Date().getTime();
if (stores.authorize.isLogin) {
config.headers['Authorization'] = `Bearer ${stores.authorize.token}`;
}
return config;
},
error: function (error) {
console.log(error); // for debug
return Promise.reject(error);
}
};
response_interceptors.js
1. 导入store,并声明两个变量来方便我们控制并发处理
import { stores } from '@/stores';
// 标记是否正在刷新令牌
let isRefreshing = false;
// 用于存储等待刷新的请求
let refreshSubscribers = [];
2. 创建刷新令牌的api方法
/**
* 令牌刷新api
* @param {*} axios
* @returns
*/
const tokenRefreshApi = function (axios) {
return axios({
method: 'post',
url: '*/refreshtoken',
data: {
refreshToken: stores.authorize.refreshToken
},
timeout: 60000
});
};
3. 声明拦截方法
由于博主的api并非使用状态码401的形式,所以此处以拦截器的success
方法为例
首先声明success方法
export default {
success: async function (resp, axios) {
}
}
4. 检验返回值
博主这里的接口响应值格式是:{ "resCode": 0, "resMsg":"" }
所以以这个格式来举例
// 判断api基础响应格式
if (resp.data && resp.data.resCode >= 0) {
return Promise.resolve(resp);
}
// 赋值默认错误返回格式
const res = {
resCode: -99,
resMsg: ''
};
// 响应格式校验
if (!resp.data) {
res.resMsg = '系统异常,请稍候再试';
} else if (resp.data.resCode == undefined || resp.data.resCode == null) {
res.resMsg = '响应异常';
} else if (resp.data.resCode === -99) {
res.resMsg = resp.data.resMsg || '操作失败';
}
5. 校验令牌过期状态,并设置并发控制
博主这边令牌过期返回状态码为-201
if (resp.data.resCode == -201){
// 首先将原有的请求赋值给变量
const originalRequest = { ...resp.config };
// 判断当前是否处于在刷新令牌情况下
if (!isRefreshing) {
// 将状态置为正在刷新令牌
isRefreshing = true;
} else {
// 已经在刷新令牌,将原始请求加入等待队列
return new Promise(resolve => {
refreshSubscribers.push(newToken => {
originalRequest.headers['Authorization'] = `Bearer ${newToken}`;
resolve(axios(originalRequest));
});
});
}
}
6. 令牌刷新
if (!isRefreshing) {
isRefreshing = true;
// 刷新令牌
if (!stores.authorize.data.refreshToken) {
// 跳转登录页
return Promise.reject({
status: 200,
data: {
...res.data
}
});
}
const resp = await tokenRefreshApi(axios);
if (!resp.data) {
// 跳转登录页
return Promise.reject({
status: 200,
data: {
...res.data
}
});
}
const res = resp.data;
if (!res || res.resCode != 0) {
// 令牌刷新跳转登录页
return Promise.reject({
status: 200,
data: {
...res.data
}
});
}
stores.authorize.login(res.data);
originalRequest.headers['Authorization'] = `Bearer ${res.data.token}`;
while (refreshSubscribers && refreshSubscribers.length) {
const req = refreshSubscribers.shift();
req(stores.authorize.data.token);
}
isRefreshing = false;
return axios(originalRequest);
} else {
// 已经在刷新令牌,将原始请求加入等待队列
return new Promise(resolve => {
refreshSubscribers.push(newToken => {
originalRequest.headers['Authorization'] = `Bearer ${newToken}`;
resolve(axios(originalRequest));
});
});
}
主要逻辑
其实无感刷新的主要注意点是在于并发控制的问题:
同时多个请求触发,该如何有效的将第一个请求结果拦截,并无感刷新令牌后,重新请求
在刷新令牌期间后续得请求进来该如何拦截,并进额外处理
完整代码
import { stores } from '@/stores';
// 标记是否正在刷新令牌
let isRefreshing = false;
// 用于存储等待刷新的请求
let refreshSubscribers = [];
/**
* 令牌刷新api
* @param {*} axios
* @returns
*/
const tokenRefreshApi = function (axios) {
return axios({
method: 'post',
url: '*/refreshtoken',
data: {
refreshToken: stores.authorize.refreshToken
},
timeout: 60000
});
};
export default {
success: async function (resp, axios) {
// const router = useRouter();
if (resp.data && resp.data.resCode >= 0) {
return Promise.resolve(resp);
}
const res = {
resCode: -99,
resMsg: ''
};
if (!resp.data) {
res.resMsg = '系统异常,请稍候再试';
} else if (resp.data.resCode == undefined || resp.data.resCode == null) {
res.resMsg = '响应异常';
} else if (resp.data.resCode === -99) {
res.resMsg = resp.data.resMsg || '操作失败';
}
if (resp.data.resCode == -21) {
const originalRequest = { ...resp.config };
if (!isRefreshing) {
isRefreshing = true;
// 刷新令牌
if (!stores.authorize.data.refreshToken) {
// 跳转登录页
return Promise.reject({
status: 200,
data: {
...res.data
}
});
}
const resp = await tokenRefreshApi(axios);
if (!resp.data) {
// 跳转登录页
return Promise.reject({
status: 200,
data: {
...res.data
}
});
}
const res = resp.data;
if (!res || res.resCode != 0) {
// 跳转登录页
return Promise.reject({
status: 200,
data: {
...res.data
}
});
}
stores.authorize.login(res.data);
originalRequest.headers['Authorization'] = `Bearer ${res.data.token}`;
while (refreshSubscribers && refreshSubscribers.length) {
const req = refreshSubscribers.shift();
req(stores.authorize.token);
}
isRefreshing = false;
return axios(originalRequest);
} else {
// 已经在刷新令牌,将原始请求加入等待队列
return new Promise(resolve => {
refreshSubscribers.push(newToken => {
originalRequest.headers['Authorization'] = `Bearer ${newToken}`;
resolve(axios(originalRequest));
});
});
}
}
resp.data = res;
return Promise.resolve(resp);
},
error: function (err) {
return Promise.reject({
status: -999,
msg: err.message
});
}
};
- 感谢你赐予我前进的力量
本网站的原创文章部分资源内容可能来源于网络,仅供大家学习与参考,如有侵权,请联系博主邮箱:zzyo.yj@outlook.com 进行删除处理
本站一切资源不代表本站立场,并不代表本站赞同其观点和对其真实性负责。
本站一律禁止以任何方式发布或转载任何违法的相关信息,访客发现请向博主举报
声明:版权所有,违者必究 | 如未注明,均为原创 | 本网站采用CC BY-NC-SA 4.0 协议进行授权
转载:转载请注明原文链接 - Lycoris