【微信小程序】微信小程序基于雙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.pswp.cn/pingmian/95293.shtml
繁體地址,請注明出處:http://hk.pswp.cn/pingmian/95293.shtml
英文地址,請注明出處:http://en.pswp.cn/pingmian/95293.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

基于單片機醉酒駕駛檢測系統/酒精檢測/防疲勞駕駛設計

傳送門 👉👉👉👉其他作品題目速選一覽表 👉👉👉👉其他作品題目功能速覽 概述 該設計基于單片機開發,旨在通過實時檢測駕駛員酒精濃度,預防酒后駕駛行為…

第6章:垃圾回收分析與調優

1. 垃圾回收基礎 1.1 Java 垃圾回收概述 垃圾回收(Garbage Collection,GC)是 Java 虛擬機自動內存管理的核心機制。理解 GC 的工作原理對于 Java 應用性能調優至關重要。 1.1.1 垃圾回收的目標 自動內存管理:無需手動釋放內存防止…

ROS2核心模塊-動作通信、參數服務

動作通信 機器人導航到某個目標點,此過程需要一個節點A發布目標信息,然后一個節點B接收到請求并控制移動,最終響應目標達成狀態信息。 乍一看,這好像是服務通信實現,因為需求中要A發送目標,B執行并返回結果&#xff0c…

word文檔封面中文件編號等標題和內容無法對齊

問題 word文檔封面中文件編號等標題和內容無法對齊,因為標題使用的是底紋不是文件內容。 解決辦法 字體大小、行距兩者配合就可以解決。

163起融資,梅卡曼德融資額奪冠,釘釘、百度智能云10周年,漢桑科技IPO| 2025年8月人工智能投融資觀察 · 極新月報

“ 二級的活躍會傳導到一級嗎?”文|云舒&小魚編輯 | 小白出品|極新8月重點關注:1、八月人工智能領域投融資事件163起,披露金額76.8億人民幣。2、億級人民幣以上金額的投資事件共20起 。3、八月人工智能領域發生一起…

微信小程序預覽和分享文件

預覽文檔previewFile(val) { let item val.currentTarget.dataset.item wx.downloadFile({url: item.filePath, // 替換為實際的文件地址success: function (res) {let filePath ${wx.env.USER_DATA_PATH}/${item.fileName}|| res.tempFilePath //查看的文件名wx.openDocumen…

開源 C++ QT Widget 開發(十二)圖表--環境監測表盤

文章的目的為了記錄使用C 進行QT Widget 開發學習的經歷。臨時學習,完成app的開發。開發流程和要點有些記憶模糊,趕緊記錄,防止忘記。 相關鏈接: 開源 C QT Widget 開發(一)工程文件結構-CSDN博客 開源…

ARMv8架構01 - ARM64架構寄存器基礎

一 、ARM64架構基礎 1 ARMv8 A 架構介紹 ARMv8 - A是ARM公司發布的第一代支持64位處理器的指令集和架構。它在擴充64位寄存器的同時提供對上一代架構指令集的兼容,因而能同時提供運行 32位 和 64位應用程序的執行環境。 超大物理地址空間(large Physical…

flutter專欄--深入剖析你的第一個flutter應用

使用fvm管理flutter版本 如果你有使用多版本flutter的需求,那么fvm將會給你提供較大的幫助。下面我列舉一下mac flutter3.35.2的版本的操作命令,完成之后,你將可以隨意切換flutter版本 # 下載fvm相關的依賴 brew tap leoafarias/fvm brew …

MongoDB 聚合查詢超時:索引優化與分片策略的踩坑記錄

人們眼中的天才之所以卓越非凡,并非天資超人一等而是付出了持續不斷的努力。1萬小時的錘煉是任何人從平凡變成超凡的必要條件。———— 馬爾科姆格拉德威爾 🌟 Hello,我是Xxtaoaooo! 🌈 “代碼是邏輯的詩篇&#xff…

Augmentcode免費額度AI開發WordPress商城實戰

Augment AI開發WordPress商城實戰:從零構建到免費額度續杯完整指南 前言 在AI編程工具日益普及的今天,如何高效利用這些工具來開發實際項目成為了開發者關注的焦點。本文將詳細介紹如何使用Augment AI從零開始構建一個功能完整的WordPress商城系統&#…

【C++八股文】數據結構篇

一、單例模式優化實現 原代碼問題分析 ?內存序重排序風險?:雙重檢查鎖在C中可能因指令重排導致半初始化對象被訪問?鎖粒度過大?:每次獲取實例都需要加鎖,影響性能?線程安全性不足?:未考慮C11前的內存模型問題 改進方案&a…

并發編程——15 線程池ForkJoinPool實戰及其工作原理分析

1 一道算法題引發的思考及其實現 1.1 算法題 問:如何充分利用多核 CPU 的性能,快速對一個2千萬大小的數組進行排序? 這道題可以通過歸并排序來解決; 1.2 什么是歸并排序? 歸并排序(Merge Sort&#xff…

Kafka面試精講 Day 6:Kafka日志存儲結構與索引機制

【Kafka面試精講 Day 6】Kafka日志存儲結構與索引機制 在“Kafka面試精講”系列的第6天,我們將深入剖析 Kafka的日志存儲結構與索引機制。這是Kafka高性能、高吞吐量背后的核心設計之一,也是中高級面試中的高頻考點。面試官常通過這個問題考察候選人是否…

Linux 字符設備驅動框架學習記錄(三)

Linux字符設備驅動開發新框架詳解 一、新舊驅動框架對比 傳統字符設備驅動流程 手動分配設備號 (register_chrdev_region)實現file_operations結構體使用mknod手動創建設備節點 新式驅動框架優勢 自動設備號分配:動態申請避免沖突自動節點創建:通過class…

《計算機網絡安全》實驗報告一 現代網絡安全挑戰 拒絕服務與分布式拒絕服務攻擊的演變與防御策略(1)

目 錄 摘 要 一、研究背景與目的 1.1 介紹拒絕服務(DoS)和分布式拒絕服務(DDoS)攻擊的背景 (1)拒絕服務攻擊(DoS)  (2)分布式拒絕服務攻擊&#xff0…

深度學習篇---模型組成部分

模型組成部分:在 PyTorch 框架下進行圖像分類任務時,深度學習代碼通常由幾個核心部分組成。這些部分中有些可以在不同網絡間復用,有些則需要根據具體任務或網絡結構進行修改。下面我將用通俗易懂的方式介紹這些組成部分:1. 數據準…

關于ANDROUD APPIUM安裝細則

1,可以先參考一下連接 PythonAppium自動化完整教程_appium python教程-CSDN博客 2,appium 需要對應的版本的node,可以用nvm對node 進行版本隔離 3,對應需要安裝android stuido 和對應的sdk ,按照以上連接進行下載安…

八、算法設計與分析

1 算法設計與分析的基本概念 1.1 算法 定義 :算法是對特定問題求解步驟的一種描述,是有限指令序列,每條指令表示一個或多個操作。特性 : 有窮性:算法需在有限步驟和時間內結束。確定性:指令無歧義&#xff…

機器學習從入門到精通 - 神經網絡入門:從感知機到反向傳播數學揭秘

機器學習從入門到精通 - 神經網絡入門:從感知機到反向傳播數學揭秘開場白:點燃你的好奇心 各位,有沒有覺得那些能識圖、懂人話、下棋碾壓人類的AI特別酷?它們的"大腦"核心,很多時候就是神經網絡!…