【前端开发】小程序无感登录验证
概述
封装的网络请求库,主要用于处理 API 请求并支持自动处理 token 过期 和 token 刷新,适用于需要身份验证的应用场景,特别是在移动端中。
主要功能
- 自动附加 Token
在每个请求中自动附加 Authorization 头部,使用存储的 accessToken。如果某个请求不需要 Token,则可以通过设置 isToken: false 来排除。
- Token 过期自动刷新
- 当请求返回提示 accessToken 过期时,自动尝试使用 refreshToken 刷新 accessToken
- 在刷新过程中,新的请求会被放入一个队列中,等到 refreshToken 成功后再依次重试
- 请求重试机制
- 当 accessToken 刷新成功后,队列中的所有待处理请求都会自动重发
- 如果 refreshToken 刷新失败,跳转到登录页面,让用户重新登录。
- 统一的请求和响应处理
- 请求的 Content-Type 和 Accept 头部统一设置
- 统一处理服务器返回的状态码,若返回 200 且不含 ErrType: -10011,则正常返回数据,否则进行相关的错误处理
代码概览
- refreshToken 函数
- 用于发送请求以刷新 accessToken,通过 refreshToken 获取新的 accessToken,并保存在本地
- 若成功,返回新的 accessToken;若失败,返回错误信息。
- request 函数
- 封装了 uni.request 请求方法,提供了 GET、POST 请求支持。
检查是否有有效的 accessToken,如果有,自动将其附加到请求头的 Authorization 中- 如果请求参数中包含 params,则自动将其转换为查询字符串附加到 URL 中
- 在请求成功时,判断返回数据的 ErrType 是否为 -10011(表示 accessToken 过期),若过期,则调用 handleTokenExpiration 函数刷新 Token
- handleTokenExpiration 函数
- 处理 accessToken 过期的逻辑,避免在刷新 Token 的过程中发起多次刷新请求
- 如果当前没有刷新请求正在进行,则调用 refreshToken 函数尝试刷新 Token,并将待重试的请求保存在队列中
- 如果刷新 Token 成功,则重试队列中的请求,重新发送
- 如果刷新 Token 失败,清空队列并跳转到登录页面
- isRefreshing 和 requestQueue
- isRefreshing 用于确保只有一个刷新请求在进行,避免并发刷新 Token 的情况
- requestQueue 用于存储等待 Token 刷新完成后的请求,确保刷新完成后再逐一处理这些请求
工作流程
- 用户首次登录时,后端会返回 accessToken 和 refreshToken
- 后续的 API 请求都会自动附带 accessToken
- 当 accessToken 过期时,后端返回 ErrType: -10011,触发 handleTokenExpiration
- handleTokenExpiration 检查是否正在进行刷新操作,如果没有,则开始刷新并保存待重试的请求
- 刷新完成后,重试这些请求,确保请求使用最新的 accessToken
Request.js
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;