【前端开发】小程序无感登录验证

概述

封装的网络请求库,主要用于处理 API 请求并支持自动处理 token 过期 和 token 刷新,适用于需要身份验证的应用场景,特别是在移动端中。

主要功能

  1. 自动附加 Token

在每个请求中自动附加 Authorization 头部,使用存储的 accessToken。如果某个请求不需要 Token,则可以通过设置 isToken: false 来排除。

  1. Token 过期自动刷新
  • 当请求返回提示 accessToken 过期时,自动尝试使用 refreshToken 刷新 accessToken
  • 在刷新过程中,新的请求会被放入一个队列中,等到 refreshToken 成功后再依次重试
  1. 请求重试机制
  • 当 accessToken 刷新成功后,队列中的所有待处理请求都会自动重发
  • 如果 refreshToken 刷新失败,跳转到登录页面,让用户重新登录。
  1. 统一的请求和响应处理
  • 请求的 Content-Type 和 Accept 头部统一设置
  • 统一处理服务器返回的状态码,若返回 200 且不含 ErrType: -10011,则正常返回数据,否则进行相关的错误处理

代码概览

  1. refreshToken 函数
  • 用于发送请求以刷新 accessToken,通过 refreshToken 获取新的 accessToken,并保存在本地
  • 若成功,返回新的 accessToken;若失败,返回错误信息。
  1. request 函数
  • 封装了 uni.request 请求方法,提供了 GET、POST 请求支持。
    检查是否有有效的 accessToken,如果有,自动将其附加到请求头的 Authorization 中
  • 如果请求参数中包含 params,则自动将其转换为查询字符串附加到 URL 中
  • 在请求成功时,判断返回数据的 ErrType 是否为 -10011(表示 accessToken 过期),若过期,则调用 handleTokenExpiration 函数刷新 Token
  1. handleTokenExpiration 函数
  • 处理 accessToken 过期的逻辑,避免在刷新 Token 的过程中发起多次刷新请求
  • 如果当前没有刷新请求正在进行,则调用 refreshToken 函数尝试刷新 Token,并将待重试的请求保存在队列中
  • 如果刷新 Token 成功,则重试队列中的请求,重新发送
  • 如果刷新 Token 失败,清空队列并跳转到登录页面
  1. isRefreshing 和 requestQueue
  • isRefreshing 用于确保只有一个刷新请求在进行,避免并发刷新 Token 的情况
  • requestQueue 用于存储等待 Token 刷新完成后的请求,确保刷新完成后再逐一处理这些请求

工作流程

  • 用户首次登录时,后端会返回 accessToken 和 refreshToken
  • 后续的 API 请求都会自动附带 accessToken
  • 当 accessToken 过期时,后端返回 ErrType: -10011,触发 handleTokenExpiration
  • handleTokenExpiration 检查是否正在进行刷新操作,如果没有,则开始刷新并保存待重试的请求
  • 刷新完成后,重试这些请求,确保请求使用最新的 accessToken

Request.js

javascript 复制代码
import store from "@/store";
import config from "@/common/config";
import { getToken, getRefreshToken, setToken } from "@/common/auth";
import { tansParams } from "@/common/index";

let timeout = 10000;
const baseUrl = config.baseUrl;

const refreshToken = () => {
  return new Promise((resolve, reject) => {
    uni
      .request({
        method: "post",
        url: `${baseUrl}/auth/refresh`,
        data: { refreshToken: getRefreshToken() },
        header: { "Content-Type": "application/json" },
      })
      .then((response) => {
        if (response.statusCode === 200 && response.data.accessToken) {
          setToken(response.data.accessToken);
          resolve(response.data.accessToken);
        } else {
          reject("刷新令牌失败");
        }
      })
      .catch((error) => {
        reject(error);
      });
  });
};

const request = (config) => {
  const isToken = (config.headers || {}).isToken === false;
  config.header = config.header || {};
  if (getToken() && !isToken) {
    config.header["Authorization"] = "Bearer " + getToken();
  }

  if (config.params) {
    let url = config.url + "?" + tansParams(config.params);
    url = url.slice(0, -1);
    config.url = url;
  }

  config.header["Content-Type"] =
    "application/x-www-form-urlencoded; charset=UTF-8";
  config.header["Accept"] = "application/json, text/javascript, */*; q=0.01";

  return new Promise((resolve, reject) => {
    uni
      .request({
        method: config.method || "get",
        timeout: config.timeout || timeout,
        url: config.baseUrl || baseUrl + config.url,
        data: config.data,
        header: config.header,
        dataType: "json",
      })
      .then((response) => {
        if (response.statusCode === 200) {
          //登录过期
          if (response.data.ErrType == "-10011") {
            // Access Token 过期
            handleTokenExpiration(config, resolve, reject);
          } else {
            resolve(response.data);
          }
        } else {
          reject("服务器连接异常");
        }
      })
      .catch((error) => {
        reject(error);
      });
  });
};

let isRefreshing = false; // 防止多次刷新
let requestQueue = []; // 队列存储待重试的请求

const handleTokenExpiration = (config, resolve, reject) => {
  if (!isRefreshing) {
    isRefreshing = true; // 开始刷新

    refreshToken()
      .then((newToken) => {
        isRefreshing = false;
        // 刷新成功后,重试队列中的请求
        requestQueue.forEach((callback) => callback(newToken));
        requestQueue = []; // 清空队列

        // 重新发送当前请求
        resolve(
          request({
            ...config,
            header: {
              ...config.header,
              Authorization: "Bearer " + newToken,
            },
          })
        );
      })
      .catch((error) => {
        isRefreshing = false;
        requestQueue = []; // 清空队列

        // 刷新失败,跳转到登录页面
        uni.reLaunch({
          url: "/pages/login",
        });
        reject("登录已过期,请重新登录");
      });
  } else {
    // 如果已经在刷新,将请求加入队列
    requestQueue.push((newToken) => {
      resolve(
        request({
          ...config,
          header: {
            ...config.header,
            Authorization: "Bearer " + newToken,
          },
        })
      );
    });
  }
};

export default request;
相关推荐
里欧跑得慢9 分钟前
Flutter 像素级还原:设计稿到代码的视觉无损转换技巧
前端·css·flutter·web
里欧跑得慢24 分钟前
Flutter 三方库 function_tree — 鸿蒙应用开发中的动态数学公式解析与计算神器,实现鸿蒙深度适配下的复杂函数逻辑运行时求值实战(适配鸿蒙 HarmonyOS Next ohos)
android·前端·安全·flutter·华为·harmonyos
苏灿烤鱼1 小时前
没有机密文件,也没有付费数据,在浏览器里造一颗"间谍卫星"
前端·javascript·github
泯泷1 小时前
手搓JSVM第 5 篇:写第一个编译器:从 AST 生成 IR
前端·javascript·算法
泯泷1 小时前
手搓JSVM第 7 篇:控制流:if、while 与 jump
前端·javascript·算法
泯泷1 小时前
手搓JSVM第 6 篇:把 IR 编成字节码:emit 与 label fixup
前端·javascript·算法
泯泷2 小时前
手搓JSVM第 3 篇:从栈式 VM 到寄存器式 VM:为什么我们选择寄存器
前端·javascript·算法
泯泷2 小时前
第 4 篇:让 VM 支持变量:Slot、Environment 与 TDZ
前端·javascript·算法
粥里有勺糖4 小时前
视野修炼-技术周刊第131期 | Bun 与 pnpm Rust 化
前端·github·agent
Coodor4 小时前
如何在web浏览器使用js操作CPU卡
开发语言·前端·javascript·cpu卡