跳转到正文
Monster Cone
返回

Flutter Dio 并发请求 Token 刷新处理方案

编辑页面

在高频访问场景中,通常使用 滚动令牌 保持用户的长时间登录状态。但是在多个并发请求同时触发 Token 刷新时,容易出现 重复刷新或刷新失败 的情况:第一个请求成功刷新了 Token,后续请求仍使用旧 Token,导致刷新失败。

本文介绍一种方案,通过 Dart 的 Completer 与 请求队列 实现并发安全的 Token 刷新处理。

1. 了解Completer

Completer 是 Dart 提供的一种手动控制 Future 完成的机制,它与 Future 的底层实现相同,但允许你在任意时刻完成或标记错误。

基本结构

class Completer<T> {
  Future<T> get future;
  void complete([T? value]);   // 标记成功
  void completeError(Object error, [StackTrace? stackTrace]); // 标记失败
}

简单示例

void main() async {
  final completer = Completer<String>();

  print("任务开始");
  // 模拟异步任务
  Future.delayed(Duration(seconds: 2), () {
    completer.complete("任务完成");
  });

  print("等待中...");
  final result = await completer.future;
  print(result);
}

执行结果:

2. 并发刷新 Token 的核心思路

  1. 定义一个刷新状态标识 _isRefreshing。

  2. 定义一个挂起请求队列 _pendingRequests,用于存储被挂起的请求及对应的 Completer。

  3. 当请求返回 401 错误时:

请求挂起示例

class _PendingRequest {
  final RequestOptions options;
  final Completer<Response> completer;

  _PendingRequest(this.options, this.completer);
}

bool _isRefreshing = false;
final List<_PendingRequest> _pendingRequests = [];

if (_instance._isRefreshing) {
  final completer = Completer<Response>();
  _instance._pendingRequests
      .add(_PendingRequest(error.requestOptions, completer));
  try {
    final res = await completer.future; // 等待刷新完成
    handler.resolve(res);
  } catch (e) {
    handler.reject(
      DioException(
        requestOptions: error.requestOptions,
        message: e.toString(),
      ),
    );
  }
  return; // 终止拦截器,防止重复处理
}

注意 return 非常重要,它会直接终止当前拦截器逻辑,避免循环触发刷新。

刷新 Token 并处理队列

_isRefreshing = true;

try {
  // 调用刷新 Token 接口
  final newToken = await AuthService.refreshToken(
    headers: {"authorization": "Bearer ${tokenManager.refreshToken}"}
  );
  await tokenManager.saveToken(newToken);

  // 取出挂起请求并清空队列
  final pending = List<_PendingRequest>.from(_pendingRequests);
  _pendingRequests.clear();

  // 重试挂起的请求
  for (final pendingRequest in pending) {
    final opts = Options(
      method: pendingRequest.options.method,
      headers: {
        ...pendingRequest.options.headers,
        "authorization": "Bearer ${tokenManager.accessToken}"
      },
      responseType: pendingRequest.options.responseType,
      contentType: pendingRequest.options.contentType,
    );

    try {
      final retryResponse = await dio.request(
        pendingRequest.options.path,
        data: pendingRequest.options.data,
        queryParameters: pendingRequest.options.queryParameters,
        options: opts,
      );

      pendingRequest.completer.complete(retryResponse);
    } catch (e) {
      pendingRequest.completer.completeError(e);
    }
  }

  // 重试当前请求
  final opts = Options(
    method: error.requestOptions.method,
    headers: {
      ...error.requestOptions.headers,
      "authorization": "Bearer ${tokenManager.accessToken}"
    },
    responseType: error.requestOptions.responseType,
    contentType: error.requestOptions.contentType,
  );

  final retryResponse = await dio.request(
    error.requestOptions.path,
    data: error.requestOptions.data,
    queryParameters: error.requestOptions.queryParameters,
    options: opts,
  );

  return handler.resolve(retryResponse);
} catch (e) {
  // 刷新失败 -> 所有挂起请求失败并登出
  for (final pendingRequest in _pendingRequests) {
    pendingRequest.completer.completeError(e);
  }
  _pendingRequests.clear();
  await logout();
  handler.reject(
    DioException(
      requestOptions: error.requestOptions,
      message: e.toString(),
    ),
  );
} finally {
  _isRefreshing = false;
}

注意: 所有挂起请求在刷新失败时都需要 completeError,否则请求将一直等待。

3. 总结

这种模式适用于高并发访问场景,并且在 Flutter + Dio 中非常实用,能有效避免重复刷新和刷新冲突的问题。

其他实现部分可以参考 Flutter Dio 封装 这篇文章


编辑页面
分享这篇文章:

上一篇
用 Cloudflare Workers 搭建 FCM 推送中转服务
下一篇
Next.js 16 中优雅地使用 SVG