Dio实现无感刷新Token(解决并发版)

38 阅读2分钟
import 'dart:async';
import 'package:dio/dio.dart';
import 'package:flutter/cupertino.dart';
import 'package:get/get.dart' hide Response;
import 'package:myapp/app/constants/app_values.dart';
import 'package:myapp/app/model/userInfo.dart';
import 'package:myapp/app/network/dio_request_retrier.dart';
import 'package:myapp/app/network/repositories/account_repository_impl.dart';
import 'package:myapp/app/routes/app_pages.dart';
import 'package:myapp/app/service/storage_service.dart';
import 'package:myapp/app/utils/logger/logger.dart';

// handler.next(...)	将请求/响应/错误 继续交给下一个拦截器
// handler.resolve(response)	手动返回一个响应结果,终止后续的错误传播
// handler.reject(error)	手动返回一个错误对象(DioException),让请求流程走错误分支
// token过期时跳转到登录页
// QueuedInterceptor,串行等待拦截器
// Token 过期自动刷新拦截器
// - 防止并发重复刷新
// - 失败时自动跳转登录
// - 刷新中其他请求会等待刷新完成再重试
class _PendingRequest {
  final RequestOptions options;
  final Completer<Response> completer;
  _PendingRequest(this.options, this.completer);
}

class ErrorInterceptor extends InterceptorsWrapper {
  bool _isRedirectingToLogin = false; // 防止重复跳转登录
  bool _isRefreshingToken = false; // 当前是否正在刷新 token
  final List<_PendingRequest> _pendingRequests = []; // 后续等待的请求队列
  @override
  void onResponse(Response response, ResponseInterceptorHandler handler) async {
    logger.i(response.requestOptions.path);
    // 检查自定义返回 code(示例:303 表示 token 过期)
    if (response.data is Map<String, dynamic> &&
        response.data['code'] == 303 &&
        response.data['data']['type'] == "need login") {
      // === 如果正在刷新 token ===
      if (_isRefreshingToken) {
        logger.w("已有刷新任务,等待刷新完成后重试请求...");
        // Completer 是 Dart 的一个 异步控制器(promise-like 对象)。
        // 它能手动决定一个 Future
        // 什么时候“完成(complete)”或“失败(completeError)”。
        final completer = Completer<Response>();
        _pendingRequests.add(
          _PendingRequest(response.requestOptions, completer),
        ); // 后续请求加入请求队列
        try {
          // 等待刷新结束(成功/失败都会唤醒),
          // 表示“刷新操作是否完成”的一个可等待对象
          final res = await completer.future; // 等待刷新完成
          handler.resolve(res);
        } catch (e) {
          logger.e("等待刷新任务失败: $e");
          return handler.reject(
            DioException(
              requestOptions: response.requestOptions,
              error: "刷新 token 失败(等待阶段): $e",
            ),
          );
        }
        return;
      }
      if (!_isRefreshingToken) {
        // === 当前是第一个发现 token 过期的请求 ===
        _isRefreshingToken = true;
        logger.w("开始刷新 token...");

        try {
          // TODO: 实际刷新token逻辑,用不带其他拦截器的dio实例进行请求刷新token
          // === 刷新完成 ===
          // 取出挂起请求并清空队列
          final pending = List<_PendingRequest>.from(_pendingRequests);
          _pendingRequests.clear();
          // 重试挂起的请求
          for (final pendingRequest in pending) {
            final retrier = DioRequestRetrier(
              requestOptions: pendingRequest.options,
            );
            try {
              final retryResponse = await retrier.retry();
              pendingRequest.completer.complete(retryResponse);
            } catch (e) {
              pendingRequest.completer.completeError(e);
            }
          }
          // 重试当前请求
          final retrier = DioRequestRetrier(
            requestOptions: response.requestOptions,
          );
          final retryResponse = await retrier.retry();
          return handler.resolve(retryResponse);
        } catch (e) {
          logger.e("刷新 token 失败: $e");
          // 刷新失败 -> 所有挂起请求失败并登出
          for (final pendingRequest in _pendingRequests) {
            pendingRequest.completer.completeError(e);
          }
          _pendingRequests.clear();
          // 跳转登录页(防止多次跳转)
          if (!_isRedirectingToLogin && Get.currentRoute != Routes.LOGIN) {
            _isRedirectingToLogin = true;
            Future.microtask(() {
              Get.offAllNamed(Routes.LOGIN);
              _isRedirectingToLogin = false;
            });
          }
          return handler.reject(
            DioException(
              requestOptions: response.requestOptions,
              error: "身份过期请重新登陆",
            ),
          );
        } finally {
          // 防止状态残留导致死锁
          _isRefreshingToken = false;
        }
      }
    }
    // 非 token 过期情况直接放行
    super.onResponse(response, handler);
  }

  @override
  void onError(DioException e, ErrorInterceptorHandler handler) {
    if (e.response?.statusCode == 401) {
      // Token 失效跳转到登录页
      // 防止重复跳转,并且确保当前不是登录页
      if (!_isRedirectingToLogin && Get.currentRoute != Routes.LOGIN) {
        _isRedirectingToLogin = true;
        // 延迟到微任务,避免 build/route 冲突
        // 它会 延后当前同步代码执行完成后再执行回调,保证:
        // 当前函数或拦截器中的逻辑先执行完
        // Flutter 的路由和上下文已经准备好
        Future.microtask(() {
          Get.offAllNamed(Routes.LOGIN);
          _isRedirectingToLogin = false;
        });
      }
      /*
      // Token 过期,使用 DioRequestRetrier 重试请求
      try {
        final retrier = DioRequestRetrier(requestOptions: e.requestOptions);
        final response = await retrier.retry();
        return handler.resolve(response);
      } catch (retryError) {
        debugPrint("Token 刷新失败: $retryError");
        return handler.reject(e);
      }
      */
      super.onError(e, handler);
    } else if (e.type == DioExceptionType.unknown) {
      // 处理网络错误
      debugPrint("网络错误: ${e.message}");
      // TODO: 添加网络错误提示
      handler.reject(e);
    } else {
      // 其他错误继续传递
      // 在 Dio 的默认实现里,super.onError()
      // 的内部实现就是 handler.next(e)
      // handler.next(e);
      super.onError(e, handler);
    }
  }
}