当前位置: 首页 > 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.dtcms.com/a/356408.html

相关文章:

  • 华为无线AC主备配置案例
  • KNN算法详解:鸢尾花识别和手写数字识别
  • mysql安全运维之常见攻击类型与防御指南-从SQL注入到权限提升
  • .Net应用程序和SqlServer数据库使用tls加密会话过程
  • DMZ层Nginx TLS 终止与安全接入配置实战20250829
  • C5仅支持20MHZ带宽,如果路由器5Gwifi处于40MHZ带宽信道时,会出现配网失败
  • Git 合并冲突
  • 【网络】snat/MASQUERADE作用和应用场景
  • 【混合开发】Android+WebView视频图片播放硬件加速详解
  • 网页提示UI操作-适应提示,警告,信息——仙盟创梦IDE
  • 嵌入式学习 day61 DHT11、I2C
  • 项目一系列-第8章 性能优化Redis基础
  • Python OpenCV图像处理与深度学习
  • 30分钟入门实战速成Cursor IDE(2)
  • 30分钟入门实战速成Cursor IDE(1)
  • 微硕WINSOK高性能NP沟道MOS管WSP4067在Type-C双向快充电源管理系统中的应用
  • Vibe Coding、AI IDE/插件
  • Ansible Playbook 实践
  • 随机森林的 “Bootstrap 采样” 与 “特征随机选择”:如何避免过拟合?(附分类 / 回归任务实战)
  • html入门教程
  • Java使用apache.commons.math3的DBSCAN实现自动聚类
  • 【Docker/Redis】服务端高并发分布式结构演进之路
  • 构建免费的音视频转文字工具:支持多语言的语音识别项目
  • OpenTenBase实战:从MySQL迁移到分布式HTAP的那些坑与收获
  • mysql双机热备(主主模式)
  • 桌面GIS软件添加第三方图层
  • 【web3】十分钟了解web3是什么?
  • Java 将HTML文件、HTML字符串转换为图片
  • 交叉编译 手动安装 libzip 库 移植ARM 需要 zlib的
  • R ggplot2学习Nature子刊一张图,换数据即可用!