fetch 可通过封装请求函数实现 token 静默刷新与重试:捕获 401 → 单例 refreshpromise 控制并发刷新 → 缓存 retryqueue 暂存失败请求 → 刷新成功后统一重放。

Fetch 本身不内置 Token 管理或自动刷新逻辑,但可以通过封装请求函数、拦截响应、结合 Promise 链与状态管理,实现 Token 过期时的静默刷新 + 失败请求重试。核心在于:捕获 401 响应 → 触发刷新 Token → 缓存待重试请求 → 刷新成功后重放请求。
用全局 Promise 缓存刷新任务,避免并发多次刷新
多个请求几乎同时收到 401 时,不能各自发起刷新请求,否则会触发多次 refreshToken 接口调用,可能被服务端拒绝或导致 Token 冲突。应确保同一时间只有一个刷新任务在执行,其余请求等待其完成。
- 声明一个 refreshPromise 变量(初始为 null),用于保存当前进行中的刷新 Promise
- 当检测到 401 且 refreshPromise 为空,调用 refreshToken 接口并赋值给它;否则直接 await 已存在的 refreshPromise
- 刷新成功后更新本地存储(如 localStorage)和内存中的 token,并将 refreshPromise 重置为 null
请求封装层统一处理认证头与错误响应
所有业务请求都走同一个 fetch 封装函数(例如 authFetch),便于集中注入 Authorization 头、拦截 401 并触发重试流程。
- 每次请求前从 localStorage 或内存读取当前 access_token,拼入 Authorization: Bearer xxx
- fetch 后检查 response.status === 401,若是则进入刷新-重试流程,而不是直接 reject
- 对非 401 错误(如网络失败、500)仍按常规方式抛出错误,不介入重试逻辑
队列暂存待重试请求,刷新后批量重放
在刷新 Token 过程中到来的新请求,不能直接发送(因为 token 无效),也不能丢弃,需暂存并等新 token 就位后统一重试。
- 维护一个 retryQueue 数组,每个元素是 { requestInfo, resolve, reject } 的对象
- 当首个 401 触发刷新时,后续所有 401 请求都推入该队列,不立即执行 fetch
- refreshPromise 成功后,遍历队列,用新 token 重新调用 authFetch,并调用对应 resolve/reject
- 注意:重试请求本身也可能失败(如新 token 也过期),此时应 reject 并清空队列,引导用户重新登录
示例关键代码结构(简化版)
以下不是完整可运行代码,而是体现核心逻辑的骨架:
let refreshPromise = null;
const retryQueue = [];
async function refreshToken() {
const refresh = localStorage.getItem('refresh_token');
const res = await fetch('/api/auth/refresh', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ refresh })
});
if (!res.ok) throw new Error('Refresh failed');
const { access_token, refresh_token } = await res.json();
localStorage.setItem('access_token', access_token);
localStorage.setItem('refresh_token', refresh_token);
return access_token;
}
async function authFetch(input, init = {}) {
const token = localStorage.getItem('access_token');
const headers = { ...init.headers, 'Authorization': `Bearer ${token}` };
const response = await fetch(input, { ...init, headers });
if (response.status === 401) {
// 开始刷新流程(仅首次触发)
if (!refreshPromise) {
refreshPromise = refreshToken().catch(err => {
// 刷新失败,清空队列,强制登出
retryQueue.forEach(({ reject }) => reject(err));
retryQueue.length = 0;
refreshPromise = null;
localStorage.removeItem('access_token');
localStorage.removeItem('refresh_token');
throw err;
});
}
// 加入重试队列,等待刷新完成
return new Promise((resolve, reject) => {
retryQueue.push({ requestInfo: { input, init }, resolve, reject });
});
}
return response;
}
// 刷新完成后重放队列(通常在 refreshToken() 成功后调用)
async function replayQueue(newToken) {
// 更新 header 中的 token(实际使用时建议重建请求)
const pending = [...retryQueue];
retryQueue.length = 0;
for (const { requestInfo, resolve, reject } of pending) {
try {
const headers = { ...requestInfo.init.headers, 'Authorization': `Bearer ${newToken}` };
const res = await fetch(requestInfo.input, { ...requestInfo.init, headers });
resolve(res);
} catch (err) {
reject(err);
}
}
}
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南











