实现前端token无感刷新
今天和盆友讨论无感刷新的这个技术,一开始我们讨论的到底是前端做还是后端做,后面公说公有理,婆说婆有理,而且现在老板喜欢提一些奇葩需求,提前准备前端来实现token无感刷新,该文章引用沐夕花开文章
分析需求
无感刷新的场景:
-
请求前判断Token是否过期,过期则刷新
-
请求后根据返回状态判断是否过期,过期则刷新
-
当在做某个操作的时候突然token失效(比如:当你想要提交某个表单信息,突然token失效,而这个表单信息有100个必填项,GG)
处理逻辑
-
判断token无感刷新
-
过期则发送刷新token请求,将新token保存,重新发送token过期请求
实现
创建一个flag isRefreshing
来判断是否刷新中
创建一个数组队列retryRequests
来保存需要重新发起的请求
判断到Token
过期
-
isRefreshing = false
的情况下 发起刷新Token
的请求-
刷新
Token
后遍历执行队列retryRequests
-
-
isRefreshing = true
表示正在刷新Token
,返回一个Pending
状态的Promise
,并把请求信息保存到队列retryRequests
中
import axios from "axios"; import Store from "@/store"; import Router from "@/router"; import { Message } from "element-ui"; import UserUtil from "@/utils/user"; // 创建实例 const Instance = axios.create(); Instance.defaults.baseURL = "/api"; Instance.defaults.headers.post["Content-Type"] = "application/json"; //这个是规定当post请求体将以JSON格式来传送 Instance.defaults.headers.post["Accept"] = "application/json"; //这告诉服务器客户端期望响应数据为 JSON 格式。 // 定义一个flag 判断是否刷新Token中 let isRefreshing = false; // 保存需要重新发起请求的队列 let retryRequests = []; // 请求拦截 Instance.interceptors.request.use(async function (config) { //这里没有看到大佬的代码 估计应该是保存在vuex中的状态 用来改变状态的方法 Store.commit("startLoading"); //这里应该是获取存储在本地的用户信息 const userInfo = UserUtil.getLocalInfo(); if (userInfo) { //业务需要把Token信息放在 params 里面,一般来说都是放在 headers里面 这里将配置对象进行重新组装 config.params = Object.assign(config.params ? config.params : {}, { appkey: userInfo.AppKey, token: userInfo.Token, }); } return config; }); // 响应拦截 Instance.interceptors.response.use( async function (response) { //此处说明请求已经完成 Store.commit("finishLoading"); //获取响应数据 const res = response.data; if (res.errcode == 0) { //如果code ===0表示请求成功 return Promise.resolve(res); } else if ( res.errcode == 30001 || res.errcode == 40001 || res.errcode == 42001 || res.errcode == 40014 ) { //此时表示请求失败 // 需要刷新Token 的状态 30001 40001 42001 40014 // 拿到本次请求的配置 let config = response.config; // 进入登录页面的不做刷新Token 处理 if (Router.currentRoute.path !== "/login") { if (!isRefreshing) { // 改变flag状态,表示正在刷新Token中 isRefreshing = true; // 刷新Token return Store.dispatch("user/refreshToken") .then((res) => { // 设置刷新后的Token config.params.token = res.Token; config.params.appkey = res.AppKey; // 遍历执行需要重新发起请求的队列 retryRequests.forEach((cb) => cb(res)); // 清空队列 retryRequests = []; return Instance.request(config); }) .catch(() => { retryRequests = []; Message.error("自动登录失败,请重新登录"); const code = Store.state.user.info.CustomerCode || ""; // 刷新Token 失败 清空缓存的用户信息 并调整到登录页面 Store.dispatch("user/logout"); Router.replace({ path: "/login", query: { redirect: Router.currentRoute.fullPath, code: code }, }); }) .finally(() => { // 请求完成后重置flag isRefreshing = false; }); } else { // 正在刷新token,返回一个未执行resolve的promise // 把promise 的resolve 保存到队列的回调里面,等待刷新Token后调用 // 原调用者会处于等待状态直到 队列重新发起请求,再把响应返回,以达到用户无感知的目的(无痛刷新) return new Promise((resolve) => { // 将resolve放进队列,用一个函数形式来保存,等token刷新后直接执行 retryRequests.push((info) => { // 将新的Token重新赋值 config.params.token = info.Token; config.params.appkey = info.AppKey; resolve(Instance.request(config)); }); }); } } return new Promise(() => {}); } else { return Promise.reject(res); } }, function (error) { let err = {}; if (error.response) { err.errcode = error.response.status; err.errmsg = error.response.statusText; } else { err.errcode = -1; err.errmsg = error.message; } Store.commit("finishLoading"); return Promise.reject(err); } ); export default Instance;
另外感谢大佬的文章介绍前端无痛刷新Token - 掘金 评论区比代码好看 争吵激烈 哈哈哈哈哈