当前位置: 首页 > news >正文

【微信小程序】微信小程序基于双token的API请求封装与无感刷新实现方案

文章目录

  • 前言
  • 一、设计思路
  • 二、执行流程
  • 三、核心模块
    • 3.1 全局配置
    • 3.2 request封装
      • 3.2.1 request方法配置参数
      • 3.2.2 请求预处理
      • 3.2.3 核心请求流程
    • 3.3 刷新accessToken
    • 3.4 辅助方法
  • 四、api封装示例
  • 总结


前言

现代前后端分离的模式中,一般都是采用token的方式实现API的鉴权,而不是传统Web应用中依赖服务器端的Session存储和客户端Cookie的自动传递匹配机制。前端发起的请求时,在其请求头内传入“Authorization:token”,后端解析请求头中的token, 获取载荷信息过期时间等状态信息,验证Token是否有效,实现鉴权。

但是token本身是具有有效性限制的,本文将实现一种微信小程序客户端在发起请求后,服务器发现token过期,客户端能自动向服务器发起请求获取最新的token,再重试上一个因为过期token而未执行的请求的流程。


一、设计思路

本文所讨论的无感刷新token的实现是基于微信小程序原生wx.request封装,采用双token的方式(accessToken + refreshToken)。accessToken生命周期短,作为请求头写入请求传给后端用于鉴权,refreshToken生命周期长,用于刷新accessToken。本方案核心目标是解决accessToken过期后,用户无感知刷新accessToken并重试请求,避免频繁跳转登录页影响体验。

并且将完善实现并发控制下的请求管理,实现单例刷新。同一时间多个请求同时出现accessToken失效,仅运行第一个请求触发刷新accessToken,最后在统一执行阻塞的请求。

这里提到的accessToken和refreshToken应当在首次成功登录之后通过setStorageSync存入本地

二、执行流程

完整流程如下:

  1. 发起请求:前端调用request方法,封装函数请求头携带accessToken
  2. 401 拦截:接口返回401,排除登录接口后,检查到存在refreshToken
  3. 状态判断:isRefreshing为false,设置为true,将刷新流程锁定,调用refreshToken函数。
  4. 刷新 Token:发起/Login/RefreshToken请求,成功后获取新accessToken,更新缓存与请求头
  5. 重试原始请求:用新accessToken重新发起之前的触发执行refreshToken逻辑的请求,成功后返回结果给前端。
  6. 队列重试:遍历requestQueue,期间可能有其他请求因401加入队列,调用每个请求的retryRequest,用新accessToken重试。
  7. 状态重置:清空requestQueue,设置isRefreshing为false,解锁刷新机制,无感刷新完成

请添加图片描述

三、核心模块

3.1 全局配置

const baseURL = 'http://localhost:806'
//请求超时时间
const timeout = 10000;
/*** 是否正在刷新token* 判断无刷新 → 锁定刷新流程 → 发起请求*/
let isRefreshing = false; // 是否正在刷新token
/*** 等待刷新token的请求队列* 刷新成功:队列中的请求需重试,重试后清空队列;* 刷新失败:队列中的请求已无意义(无有效 token 可用),直接清空队列;* 刷新过程中:队列不能重置(需保留等待的请求)。*/
let requestQueue = [];

isRefreshing和requestQueue是两个关键全局变量来实现并发控制与请求管理

  • isRefreshing(bool):标记是否正在发起 Token 刷新请求,防止同一时间多个请求触发重复刷新
  • requestQueue(array):存储Token刷新期间发起的请求,刷新成功后统一重试,保证请求完整性与用户无感知。

3.2 request封装

封装一个基于原生wx.request的函数,作为所有接口请求的入口,负责请求参数处理、Token 携带、401 拦截、队列管理。

3.2.1 request方法配置参数

通过一个默认的配置项实现构造函数的职能,优先使用具体的api请求方法里配置项。

export function request(options) {const {url,                  //接口路径(相对路径)method = 'GET',       //请求方法(GET/POST 等)data = null,          //请求参数header = {},          //自定义请求头isShowLoading = true, //是否显示加载中弹窗isNeedToken = true,   //是否需要携带Access TokenretryCount = 0,       //当前重试次数maxRetry = 1,         //最大重试次数} = options/*** 省略*/
}

3.2.2 请求预处理

let requestUrl = url;let requestData = data;const requestHeader = {'Content-Type': 'application/json', // 默认JSON格式...header // 允许用户覆盖默认头}// 处理GET请求的参数if (method === 'get' && data) {// 将参数序列化为查询字符串const queryString = Object.keys(data).map(key => `${encodeURIComponent(key)}=${encodeURIComponent(data[key])}`).join('&');requestUrl += `?${queryString}`;requestData = null; // 清空data字段,因为已经将参数拼接到url中了}if (isShowLoading) {wx.showLoading({title: "加载中",mask: true  //开启蒙版遮罩});}if (isNeedToken) {const token = wx.getStorageSync('accessToken');if (token) { // 仅当token存在时添加requestHeader['Authorization'] = `Bearer ${token }`;}}

3.2.3 核心请求流程

解析服务器的响应,通过是否是非登录请求的401,来判断上一个请求无访问权限,需要获取新的token。

  • 步骤1:无refreshToken标志彻底过期,跳转登录
  • 步骤2:封装当前请求的重试逻辑,在获取到新的Token后重新发起当前请求
  • 步骤3:根据刷新状态,决定是立刻发起刷新token逻辑还是加入到待执行请求的队列里
  • 步骤4:执行刷新accessToken的逻辑

进入刷新accessToken的逻辑时,需要锁定刷新入口,保证仅有一个请求能进入刷新流程。并且在执行刷新accessToken的逻辑后需要回调重试队列中的所有请求,重试完成后清空队列

//返回Promise对象return new Promise((resolve, reject) => {wx.request({url: baseURL + requestUrl,timeout: timeout,method: method,data: requestData,header: requestHeader,success: (res) => {//非登录请求,并且响应状态码是401,说明无访问权限,需要获取新的tokenif (res.statusCode == 401 && url != "loginEncrypt") {const _refreshToken = wx.getStorageSync('refreshToken');//步骤1:无refreshToken标志彻底过期,跳转登录if (!_refreshToken) {if (getCurrentPage() !== 'pages/login/login') {wx.navigateTo({ url: '/pages/login/login' });}return;}//步骤2:封装当前请求的重试逻辑,在获取到新的Token后重新发起当前请求const retryRequest = () => {//如果新token仍无效,额外再触发if (retryCount >= maxRetry) {reject(new Error('超过最大重试次数'));return;}//用新token重新发起当前请求request({...options,isShowLoading: false, // 避免重复显示loadingretryCount: retryCount + 1}).then(resolve).catch(reject);};//步骤3:根据刷新状态,决定是立刻发起刷新token逻辑还是加入到待执行请求的队列里if (isRefreshing) {//正在刷新token,将当前请求加入队列等待requestQueue.push(retryRequest);}else {//锁定刷新,保证仅有一个请求能进入刷新流程isRefreshing = true;//刷新tokenlet requestParms = {url: url,data: requestData,method: method,header: requestHeader,};//步骤4:执行刷新accessToken的逻辑refreshToken(requestParms, (result) => {resolve(result);//刷新成功后,重试队列中的所有请求requestQueue.forEach(async (retry) => {try { await retry(); } catch (err) { console.error('队列请求重试失败:', err); }});//重试完成后清空队列requestQueue = [];}, reject);}}//说明是正常请求else {resolve(res.data);}},fail: (res) => {wx.showToast({title: '请求数据失败,请稍后重试。',icon: 'error',duration: 2000});reject(res);},complete: () => {wx.hideLoading();}})})

3.3 刷新accessToken

accessToken刷新函数是实现无感刷新的一个重要组成。它主要是用来发起刷新accessToken请求、更新accessToken缓存、并且重试队列请求。

  • 步骤1:refreshToken标志登录信息的彻底失效,需要重新执行登录验证,清空队列,释放accessToken的刷新
  • 步骤2:重试本次因accessToken失效无法正常响应的请求
  • 步骤3:刷新成功后,重试队列中的所有请求【执行刷新Token中进入队列的请求】

执行刷新token的时候,把accessToken和refreshToken同时传入,用于比较二者是否匹配,防止出现refreshToken泄漏导致的刷新漏洞。

function refreshToken(requestParms, outResolve, outReject) {const _refreshToken = wx.getStorageSync('refreshToken');// 发起刷新Token的请求wx.request({url: baseURL + '/Login/RefreshToken',timeout: timeout,method: 'POST',header: requestParms.header,data: {refreshToken: _refreshToken},success: (res) => {//步骤1:refreshToken标志登录信息的彻底失效,需要重新执行登录验证,清空队列,释放accessToken的刷新if (res.statusCode != 200) {wx.showToast({title: res.data.msg,icon: 'none'});//刷新失败:清空队列requestQueue = [];//解锁刷新isRefreshing = false;//跳转登录setTimeout(() => {// 跳转登录if (getCurrentPage() !== 'pages/login/login') {wx.navigateTo({ url: '/pages/login/login' });}}, 2000);return;}//步骤2:重试本次因accessToken失效无法正常响应的请求wx.setStorageSync('accessToken', res.data.data);requestParms.header['Authorization'] = 'Bearer ' + res.data.data;wx.request({url: baseURL + requestParms.url,timeout: timeout,method: requestParms.method,data: requestParms.data,header: { ...requestParms.header },success: (res) => {outResolve(res.data);},fail: (res) => {wx.showToast({title: res.data.msg ? res.data.msg : '请求数据失败,请稍后重试',icon: 'error',duration: 2000});outReject(res); // 通知外层失败},complete: () => {// 刷新完成:重置状态(无论成功失败)isRefreshing = false;}})},fail: () => {// 刷新失败:清空队列,重置状态requestQueue = [];isRefreshing = false;// 请求失败,需要重新登录if (getCurrentPage() !== 'pages/login/login') {wx.navigateTo({ url: '/pages/login/login' });}}});
}

3.4 辅助方法

用于获取当前页面的路径。

/*** 获取当前页面路径*/
function getCurrentPage() {const pages = getCurrentPages();return pages[pages.length - 1]?.route || '';
}

四、api封装示例

目录结构

miniprogram/
├── api/
│   ├── modules/
│   │   ├── auth/
│   │       └── index.js
│   ├── index.js
│   └── request.js
└── pages/└── login/└── login.js 

api -> auth -> index.js示例

import { request } from "../../../api/request";// 加密登录
export function login(params) {return request({url: '/Auth/Login',method: 'post',data: params})
}

api -> index.js示例

export * as authApi from './modules/auth/index';

login.js示例

import { authApi } from '../../api/index';
authApi.login({encryptStr: _encryptStr}).then(res => {}

完整request.js代码

// 全局请求封装
//接口基础地址
const baseURL = 'http://localhost:806'
//请求超时时间
const timeout = 10000;
/*** 是否正在刷新token* 判断无刷新 → 锁定刷新流程 → 发起请求*/
let isRefreshing = false; // 是否正在刷新token
/*** 等待刷新token的请求队列* 刷新成功:队列中的请求需重试,重试后清空队列;* 刷新失败:队列中的请求已无意义(无有效 token 可用),直接清空队列;* 刷新过程中:队列不能重置(需保留等待的请求)。*/
let requestQueue = [];/*** 请求封装* @param {*} options */
export function request(options) {const {url,                  //接口路径(相对路径)method = 'GET',       //请求方法(GET/POST 等)data = null,          //请求参数header = {},          //自定义请求头isShowLoading = true, //是否显示加载中弹窗isNeedToken = true,   //是否需要携带Access TokenretryCount = 0,       //当前重试次数maxRetry = 1,         //最大重试次数} = optionslet requestUrl = url;let requestData = data;const requestHeader = {'Content-Type': 'application/json', // 默认JSON格式...header // 允许用户覆盖默认头}// 处理GET请求的参数if (method === 'get' && data) {// 将参数序列化为查询字符串const queryString = Object.keys(data).map(key => `${encodeURIComponent(key)}=${encodeURIComponent(data[key])}`).join('&');requestUrl += `?${queryString}`;requestData = null; // 清空data字段,因为已经将参数拼接到url中了}if (isShowLoading) {wx.showLoading({title: "加载中",mask: true  //开启蒙版遮罩});}if (isNeedToken) {const token = wx.getStorageSync('accessToken');if (token) { // 仅当token存在时添加requestHeader['Authorization'] = `Bearer ${token}`;}}//返回Promise对象return new Promise((resolve, reject) => {wx.request({url: baseURL + requestUrl,timeout: timeout,method: method,data: requestData,header: requestHeader,success: (res) => {//非登录请求,并且响应状态码是401,说明无访问权限,需要获取新的tokenif (res.statusCode == 401 && url != "loginEncrypt") {const _refreshToken = wx.getStorageSync('refreshToken');//步骤1:无refreshToken标志彻底过期,跳转登录if (!_refreshToken) {if (getCurrentPage() !== 'pages/login/login') {wx.navigateTo({ url: '/pages/login/login' });}return;}//步骤2:封装当前请求的重试逻辑,在获取到新的Token后重新发起当前请求const retryRequest = () => {//如果新token仍无效,额外再触发if (retryCount >= maxRetry) {reject(new Error('超过最大重试次数'));return;}//用新token重新发起当前请求request({...options,isShowLoading: false, // 避免重复显示loadingretryCount: retryCount + 1}).then(resolve).catch(reject);};//步骤3:根据刷新状态,决定是立刻发起刷新token逻辑还是加入到待执行请求的队列里if (isRefreshing) {//正在刷新token,将当前请求加入队列等待requestQueue.push(retryRequest);}else {//锁定刷新,保证仅有一个请求能进入刷新流程isRefreshing = true;//刷新tokenlet requestParms = {url: url,data: requestData,method: method,header: requestHeader,};//步骤4:执行刷新accessToken的逻辑refreshToken(requestParms, (result) => {resolve(result);//刷新成功后,重试队列中的所有请求requestQueue.forEach(async (retry) => {try { await retry(); } catch (err) { console.error('队列请求重试失败:', err); }});//重试完成后清空队列requestQueue = [];}, reject);}}//说明是正常请求else {resolve(res.data);}},fail: (res) => {wx.showToast({title: '请求数据失败,请稍后重试。',icon: 'error',duration: 2000});reject(res);},complete: () => {wx.hideLoading();}})})
}/*** 刷新token* @param {*} requestParms * @param {*} outResolve */
function refreshToken(requestParms, outResolve, outReject) {const _refreshToken = wx.getStorageSync('refreshToken');// 发起刷新Token的请求wx.request({url: baseURL + '/Login/RefreshToken',timeout: timeout,method: 'POST',header: requestParms.header,data: {refreshToken: _refreshToken},success: (res) => {//步骤1:refreshToken标志登录信息的彻底失效,需要重新执行登录验证,清空队列,释放accessToken的刷新if (res.statusCode != 200) {wx.showToast({title: res.data.msg,icon: 'none'});//刷新失败:清空队列requestQueue = [];//解锁刷新isRefreshing = false;//跳转登录setTimeout(() => {// 跳转登录if (getCurrentPage() !== 'pages/login/login') {wx.navigateTo({ url: '/pages/login/login' });}}, 2000);return;}//步骤2:重试本次因accessToken失效无法正常响应的请求wx.setStorageSync('accessToken', res.data.data);requestParms.header['Authorization'] = 'Bearer ' + res.data.data;wx.request({url: baseURL + requestParms.url,timeout: timeout,method: requestParms.method,data: requestParms.data,header: { ...requestParms.header },success: (res) => {outResolve(res.data);},fail: (res) => {wx.showToast({title: res.data.msg ? res.data.msg : '请求数据失败,请稍后重试',icon: 'error',duration: 2000});outReject(res); // 通知外层失败},complete: () => {// 刷新完成:重置状态(无论成功失败)isRefreshing = false;}})},fail: () => {// 刷新失败:清空队列,重置状态requestQueue = [];isRefreshing = false;// 请求失败,需要重新登录if (getCurrentPage() !== 'pages/login/login') {wx.navigateTo({ url: '/pages/login/login' });}}});
}/*** 获取当前页面路径*/
function getCurrentPage() {const pages = getCurrentPages();return pages[pages.length - 1]?.route || '';
}

总结

该方案通过封装微信小程序wx.request,结合双token机制与并发请求队列管理,实现了token过期后的无感刷新与请求重试。

http://www.xdnf.cn/news/1385803.html

相关文章:

  • Unity、Unreal Engine与Godot中纹理元数据管理的比较分析
  • uni-app + Vue3 开发H5 页面播放海康ws(Websocket协议)的视频流
  • 腾讯位置商业授权微信小程序距离计算
  • 有鹿机器人:用智能清洁重塑多行业工作方式
  • AI推介-大语言模型LLMs论文速览(arXiv方向):2025.04.25-2025.04.30
  • ADO 操作access
  • 选华为实验工具:eNSP Pro 和社区在线实验哪个更适合?
  • 《华为战略管理法:DSTE 实战体系》读书笔记
  • 第二章 Vue + Three.js 实现鼠标拖拽旋转 3D 立方体交互实践
  • FDTD_mie散射_项目研究(1)
  • DirectX修复工具官方中文增强版下载!下载安装教程(附安装包),0xc000007b错误解决办法
  • 【python+requests】接口自动化测试:三步用代理工具快速定位问题
  • Linux 软件编程(十四)网络编程:数据存储与 SQLite 数据库
  • 【C++】类与对象(上)
  • Python- Visual Studio Code配置Anaconda
  • Vue 实战:优雅实现无限层级评论区,支持“显示全部”分页递归加载
  • simd笔记
  • 使用生成对抗网络增强网络入侵检测性能
  • 【开题答辩全过程】以 基于Python的美食点评系统为例,包含答辩的问题和答案
  • 【数据结构与算法-Day 20】从零到一掌握二叉树:定义、性质、特殊形态与存储结构全解析
  • Hadoop(六)
  • T06_循环神经网络
  • 基于博客系统的自动化测试项目
  • Selenium无法定位元素的几种解决方案
  • C# 日志写入loki
  • 力扣452:用最少数量的箭射爆气球(排序+贪心)
  • 如何编译和使用 tomcat-connectors-1.2.32 源码(连接 Apache 和 Tomcat)​附安装包下载
  • 数据质检之springboot通过yarn调用spark作业实现数据质量检测
  • Dify 1.8.0 全网首发,预告发布
  • 2024-06-13-debian12安装Mariadb-Galera-Cluster+Nginx+Keepalived高可用多主集群