You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Dio拦截器陷入循环触发401错误,如何按请求前置周期刷新Token?

解决Token刷新循环及请求前自动校验有效期的问题

问题根源分析

你的代码存在几个核心问题,直接导致了刷新循环和不必要的登录跳转:

  • 调用refreshToken()时没有使用await,代码会直接跳过刷新流程发送原请求,此时Token还是旧的无效值,触发401错误后进入登录跳转。
  • 刷新Token的请求本身会被AuthInterceptor拦截(默认需要Token),导致不断触发刷新逻辑,形成死循环。
  • 401/403错误处理过于激进,没有区分"Token过期待刷新"和"刷新失败"两种场景,直接跳转登录。

修复方案与代码实现

针对上述问题,我们从异步等待、拦截排除、队列管理、错误分层处理四个维度调整代码:

修复后的AuthInterceptor

class AuthInterceptor extends QueuedInterceptorsWrapper {
  final Dio _dio;
  late final IUserServices _loginManager = UserServices();

  bool _isRefreshing = false;
  // 存储刷新过程中到来的待处理请求
  final Queue<RequestInterceptorHandler> _pendingRequests = Queue();

  AuthInterceptor(this._dio);

  @override
  Future<void> onRequest(RequestOptions options, RequestInterceptorHandler handler) async {
    if (options.headers["requiresToken"] == false) {
      options.headers.remove("requiresToken");
      return handler.next(options);
    }

    // 确认Token剩余有效期(确保getExpireTime返回的是剩余秒数)
    final int remainingTime = TokenExpireHelper().getExpireTime();
    bool needRefresh = remainingTime < 3580;

    if (needRefresh) {
      if (!_isRefreshing) {
        _isRefreshing = true;
        try {
          // 等待Token刷新完成再继续
          final refreshSuccess = await _loginManager.refreshToken();
          if (refreshSuccess) {
            // 更新当前请求的Token
            options.headers['Authorization'] = 'Bearer ${access_token.$}';
            // 批量处理所有等待中的请求
            _processPendingRequests();
          } else {
            _navigateToLogin();
            return handler.reject(DioException(requestOptions: options, message: "Token刷新失败"));
          }
        } catch (e) {
          _navigateToLogin();
          return handler.reject(DioException(requestOptions: options, error: e));
        } finally {
          _isRefreshing = false;
        }
      } else {
        // 正在刷新,将请求加入等待队列
        _pendingRequests.add(handler);
        return;
      }
    } else {
      // Token有效,直接设置请求头
      options.headers['Authorization'] = 'Bearer ${access_token.$}';
    }

    print('REQUEST[${options.method}] => PATH: ${options.path}');
    return handler.next(options);
  }

  @override
  Future<void> onError(DioException e, ErrorInterceptorHandler handler) async {
    if (e.response?.statusCode == 401 || e.response?.statusCode == 403) {
      // 处理Token已失效但未提前刷新的场景
      if (!_isRefreshing) {
        _isRefreshing = true;
        try {
          final refreshSuccess = await _loginManager.refreshToken();
          if (refreshSuccess) {
            // 刷新成功,重试原请求
            final newOptions = e.requestOptions.copyWith(
              headers: {...e.requestOptions.headers, 'Authorization': 'Bearer ${access_token.$}'}
            );
            final response = await _dio.fetch(newOptions);
            return handler.resolve(response);
          } else {
            _navigateToLogin();
          }
        } catch (refreshError) {
          _navigateToLogin();
        } finally {
          _isRefreshing = false;
        }
      } else {
        // 正在刷新,将错误请求加入队列,刷新完成后重试
        _pendingRequests.add(handler);
        return;
      }
    }
    print(e.response?.statusCode);
    return handler.next(e);
  }

  // 处理等待队列中的所有请求
  void _processPendingRequests() {
    while (_pendingRequests.isNotEmpty) {
      final handler = _pendingRequests.removeFirst();
      final options = handler.requestOptions;
      options.headers['Authorization'] = 'Bearer ${access_token.$}';
      handler.next(options);
    }
  }

  // 统一登录跳转逻辑,避免重复代码
  void _navigateToLogin() {
    OneContext.instance.key.currentState?.pushNamedAndRemoveUntil("/signInView", (Route<dynamic> route) => false);
  }
}

修复后的refreshToken函数

@override
Future<bool> refreshToken() async {
  try {
    final response = await _reqresManager.post(
      _RezyonRequestPaths.refresh.name,
      options: Options(
        headers: {
          "Content-Type": "application/json",
          "Authorization": "Bearer ${access_token.$}",
          // 添加标识,让拦截器跳过对刷新请求的处理,打破循环
          "requiresToken": false,
        },
      ),
    );

    if (response.statusCode == HttpStatus.ok) {
      final String accessToken = response.data["access_token"];
      final int expireDate = response.data["expires_in"];

      // 直接更新存储(若状态管理是响应式的,无需先load)
      access_token.$ = accessToken;
      await access_token.save();

      expires_in.$ = expireDate;
      await expires_in.save();

      print("refresh token works");
      return true;
    }
    return false;
  } catch (e) {
    print("Token刷新失败: $e");
    return false;
  }
}

关键优化点说明

  1. 异步等待刷新完成:使用await确保拿到新Token后再处理请求,避免旧Token导致的401。
  2. 排除刷新请求的拦截:给刷新请求添加requiresToken: false标识,防止被拦截器重复处理。
  3. 请求队列管理:用_pendingRequests队列存储刷新过程中的请求,刷新完成后批量处理,避免请求丢失或重复刷新。
  4. 分层错误处理:只有当刷新Token失败时才跳转登录,正常请求的401会尝试刷新后重试,提升用户体验。

内容的提问来源于stack exchange,提问作者miracledev

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.02 00:07:50