news 2026/5/23 22:14:43

401 刷新 Token 的队列版(请求挂起排队 + 刷新后统一重放/统一失败)

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
401 刷新 Token 的队列版(请求挂起排队 + 刷新后统一重放/统一失败)

——更工程化的“中间件”语义,适合中大型项目

共享 Future 方案已经够用;队列版适用于:

  • 想在 refresh 期间“挂起请求”,不立刻抛错

  • 想刷新失败时“一锅端”所有等待请求

  • 想控制重试节奏(顺序/限流/并发重放)

1. 队列版核心思想

当请求 401:

  1. 不立刻handler.next(err)

  2. 把这个请求的(RequestOptions + handler)存入队列

  3. 触发一次 refresh(并发锁保证只一次)

  4. refresh 成功:统一重放队列里所有请求(每个 resolve 回原来的 caller)

  5. refresh 失败:统一 reject(并触发全局登出)

2. 代码实现(可直接用)

2.1 事件总线(可选,但强烈推荐)

import 'dart:async'; enum AuthEvent { expired } class AuthEventBus { AuthEventBus._(); static final AuthEventBus I = AuthEventBus._(); final _c = StreamController<AuthEvent>.broadcast(); Stream<AuthEvent> get stream => _c.stream; void emit(AuthEvent e) => _c.add(e); }

2.2 队列元素

import 'package:dio/dio.dart'; class _QueuedReq { final RequestOptions options; final ErrorInterceptorHandler handler; _QueuedReq(this.options, this.handler); }

2.3 队列版 RefreshInterceptor

import 'package:dio/dio.dart'; class QueueRefreshInterceptor extends Interceptor { final Dio dio; final Dio cleanDio; final RefreshManager mgr; final List<_QueuedReq> _queue = []; bool _expiredEmitted = false; QueueRefreshInterceptor({ required this.dio, required this.cleanDio, required this.mgr, }); @override void onError(DioException err, ErrorInterceptorHandler handler) async { if (err.response?.statusCode != 401) { return handler.next(err); } final req = err.requestOptions; // 防死循环:同一请求只重放一次 if (req.extra["retried"] == true) { return handler.next(err); } final pair = TokenStore.get(); if (pair == null || pair.refreshToken.isEmpty) { _emitExpiredOnce(); return handler.next(err); } // ① 入队 + 挂起(此刻不 next,不 resolve) _queue.add(_QueuedReq(req, handler)); // ② 触发“只一次”的刷新 final ok = await mgr.getOrCreate(() async { try { final res = await cleanDio.post("/auth/refresh", data: { "refreshToken": pair.refreshToken, }); final data = res.data as Map<String, dynamic>; final access = data["accessToken"] as String; final refresh = data["refreshToken"] as String; await TokenStore.set(TokenPair(access, refresh)); return access; // 返回非空代表成功 } catch (_) { await TokenStore.clear(); return null; } }); // ③ 注意:多个 onError 都会走到这里。为了避免重复 drain: if (_queue.isEmpty) return; final pending = List<_QueuedReq>.from(_queue); _queue.clear(); // ④ 刷新失败:统一失败 + 全局登出事件 if (ok == null) { _emitExpiredOnce(); for (final q in pending) { q.handler.next(err); } return; } // ⑤ 刷新成功:统一重放队列请求(这里选择顺序重放,最稳) for (final q in pending) { try { final resp = await _replay(q.options); q.handler.resolve(resp); } catch (e) { q.handler.next(e is DioException ? e : err); } } } Future<Response<dynamic>> _replay(RequestOptions req) { final token = TokenStore.get()?.accessToken ?? ""; return dio.request( req.path, data: req.data, queryParameters: req.queryParameters, options: Options( method: req.method, headers: Map<String, dynamic>.from(req.headers) ..["Authorization"] = "Bearer $token", extra: Map<String, dynamic>.from(req.extra)..["retried"] = true, ), cancelToken: req.cancelToken, onSendProgress: req.onSendProgress, onReceiveProgress: req.onReceiveProgress, ); } void _emitExpiredOnce() { if (_expiredEmitted) return; _expiredEmitted = true; AuthEventBus.I.emit(AuthEvent.expired); } }

3. 队列版怎么接入 DioClient?

class DioClient { DioClient._(); static final DioClient instance = DioClient._(); late final Dio dio; late final Dio cleanDio; final RefreshManager mgr = RefreshManager(); void init() { dio = Dio(BaseOptions(baseUrl: "https://api.example.com")); cleanDio = Dio(BaseOptions(baseUrl: "https://api.example.com")); dio.interceptors.addAll([ AuthInterceptor(), QueueRefreshInterceptor(dio: dio, cleanDio: cleanDio, mgr: mgr), ]); } }

4. 共享 Future 版 vs 队列版怎么选?

  • 小中型项目:共享 Future 版足够(简单稳定)

  • 中大型项目:队列版更工程化(挂起/统一重放/统一失败)

结语:你真正学会的是“异步并发控制”

这套方案的本质不是 Dio,而是:

  • 共享 Future 作为异步锁

  • 临界区只执行一次

  • 失败请求恢复(retry / replay)

  • 全局状态一致(expired 事件)

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/5/22 4:10:07

基于数据库的学情分析网站设计开题报告

2021级本科毕业设计&#xff08;交叉复合型论文&#xff09;开题报告表学号&#xff1a;姓名&#xff1a;学院&#xff1a;信息与通信工程学院第一专业&#xff1a;第一导师姓名、职称&#xff1a;第二专业&#xff1a;第二导师姓名、职称&#xff1a;论文题目题目来源(打勾选择…

作者头像 李华
网站建设 2026/5/15 18:21:24

anything-llm全功能RAG系统助力企业智能化升级

Anything LLM&#xff1a;重塑企业知识智能的RAG实践 在企业数字化转型的深水区&#xff0c;一个看似简单却长期无解的问题反复浮现&#xff1a;如何让员工快速、准确地获取组织内部散落在PDF、手册、邮件和共享盘中的知识&#xff1f;传统搜索工具面对非结构化文档束手无策&am…

作者头像 李华
网站建设 2026/5/23 18:34:01

PE-Labeled CEACAM-5/CD66e FcAvi Tag:上皮癌诊疗的“模块化多功能导航

PE-Labeled CEACAM-5/CD66e Fc&Avi Tag 是一种针对癌胚抗原家族关键成员设计的高级重组蛋白探针。癌胚抗原相关细胞粘附分子5是免疫球蛋白超家族的成员&#xff0c;在正常成人结肠黏膜等上皮组织有痕量表达&#xff0c;但在结直肠癌、非小细胞肺癌、胃癌、乳腺癌及胰腺癌等…

作者头像 李华
网站建设 2026/5/23 18:33:38

Open-AutoGLM如何实现电脑全自动操控?99%的人都不知道的5大核心技术

第一章&#xff1a;Open-AutoGLM如何实现电脑全自动操控&#xff1f;Open-AutoGLM 是一个基于自然语言理解与自动化执行框架的开源项目&#xff0c;旨在通过大语言模型驱动操作系统级任务&#xff0c;实现真正意义上的电脑全自动操控。其核心机制是将用户输入的自然语言指令解析…

作者头像 李华
网站建设 2026/5/23 9:26:45

anything-llm能否用于游戏剧情生成?互动叙事应用测试

Anything-LLM能否用于游戏剧情生成&#xff1f;互动叙事应用测试 在一款开放世界角色扮演游戏中&#xff0c;玩家做出了一个出人意料的选择&#xff1a;他没有拯救被绑架的盟友&#xff0c;反而与敌对势力达成交易。编剧团队原本并未为此设计后续分支——但游戏中的NPC却自然地…

作者头像 李华
网站建设 2026/5/15 18:09:53

LangFlow AppDynamics End User Monitoring

LangFlow 与 AppDynamics&#xff1a;构建可监控的 AI 工作流 在生成式 AI 快速渗透企业应用的今天&#xff0c;一个现实问题日益凸显&#xff1a;如何让复杂的语言模型工作流不仅“跑得起来”&#xff0c;还能“看得清楚”&#xff1f;传统的 LLM 应用开发往往止步于功能实现&…

作者头像 李华