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

Flutter使用Dio拦截器刷新Token时出现无限循环问题

问题:Dio拦截器刷新Token时出现无限循环

我在尝试用Dio拦截器实现Token刷新,考虑到并发请求场景用了QueuedInterceptorsWrapper。Token过期后程序能进入错误拦截器,调用刷新Token接口后也会打印“Token Refreshlendi!”日志,但后续再次发起API请求时会触发无限循环。以下是我的ApiService代码:

class ApiService {
  final Dio _dio = Dio();
  final Dio tokenDio = Dio();
  int tryCount = 0;

  String? accessToken;
  String? refreshToken;

  final _storage = const FlutterSecureStorage();

  ApiService() {
    _dio.options.baseUrl = ApiConstants.baseUrl;
    tokenDio.options = _dio.options;
    _storage.read(key: "token").then((value) => accessToken = value);
    _storage.read(key: "refreshToken").then((value) => refreshToken = value);
    _dio.interceptors.add(
      QueuedInterceptorsWrapper(
        onError: (DioError error, ErrorInterceptorHandler handler) async {
          if (error.response?.statusCode == 401) {
            log("Hata Aldık! ${error.requestOptions.path}");
            RequestOptions options = error.response!.requestOptions;
            if ("Bearer $accessToken" != options.headers[HttpHeaders.authorizationHeader]) {
              log("token farklıydı! $accessToken / ${options.headers[HttpHeaders.authorizationHeader]} ");
              options.headers[HttpHeaders.authorizationHeader] = "Bearer $accessToken";
              _dio.fetch(options).then(
                (value) => handler.resolve(value),
                onError: (e) {
                  log("error on resolve");
                  handler.reject(e);
                },
              );
              return;
            }
            _dio
                .get("/Auth/RefreshTokenLogin?refreshToken=${Uri.encodeQueryComponent(refreshToken!)}")
                .then((response) async {
              log("Token Refreshlendi!");

              if (response.statusCode == 200) {
                final RefreshTokenResponseModel? model = RefreshTokenResponseModel.fromJson(response.data);

                _storage.write(key: "token", value: model!.data!.token);
                _storage.write(key: "refreshToken", value: model.data!.refreshToken);
                refreshToken = model.data!.refreshToken;
                accessToken = model.data!.token;
                options.headers[HttpHeaders.authorizationHeader] = "Bearer ${model.data!.token}";
                tokenDio.options.headers = options.headers;

                // await UserSecureStorage.setField("token", model!.data!.token);
                // await UserSecureStorage.setField("refreshToken", model.data!.refreshToken);

                // final AuthService _authService = AuthService();
                // await _authService.registerDevice();

                if (model.data != null && model.data!.contractsToBeApproved!.isNotEmpty) {
                  final AuthController authController = Get.Get.find();
                  authController.contractsToBeApproved.value = model.data!.contractsToBeApproved!;
                }

                _dio.fetch(options).then(
                  (value) => handler.resolve(value),
                  onError: (e) {
                    handler.reject(e);
                  },
                );
                return;
              }
              return;
            });
          }
          return handler.next(error);
 
        },
        onRequest: (RequestOptions options, RequestInterceptorHandler handler) {
          log("istek atıldı.  ${options.path}");

          if (accessToken == null) {
            _storage.read(key: "token").then((value) {
              log("storage readed $value");
              accessToken = value;
              options.headers[HttpHeaders.authorizationHeader] = "Bearer $value";
              tokenDio.options.headers = options.headers;
              handler.next(options);
            });
          } else {
            options.headers[HttpHeaders.authorizationHeader] = "Bearer $accessToken";
            return handler.next(options);
          }
        },
      ),
    ); 
  }
}

问题根源分析

1. 刷新Token请求触发拦截器循环

你用带拦截器的_dio实例发起刷新Token请求,当旧Token过期时,这个刷新请求本身也会返回401,再次进入错误拦截器,导致无限循环。虽然定义了tokenDio(不带拦截器的实例),但实际没用到它来发起刷新请求。

2. 未处理刷新Token失败的场景

如果刷新Token接口返回非200(比如refreshToken已过期),代码没有做任何终止逻辑,后续请求依然用旧Token,持续触发401错误,形成循环。

3. 异步操作未正确等待,状态不一致

  • onError中使用.then()而非await处理异步请求,导致Token更新逻辑可能还没完成,就重新发起了原请求。
  • onRequest中读取Storage的异步操作没有等待完成就可能让请求发出,或者多个并发请求重复读取,导致Token状态不一致。

4. 缺少重试次数限制

定义了tryCount变量但未使用,没有限制重试次数,一旦刷新逻辑出错,会无限重试。


修复后的代码示例

class ApiService {
  final Dio _dio = Dio();
  final Dio tokenDio = Dio();
  int _retryCount = 0;
  static const int _maxRetryCount = 1;

  String? accessToken;
  String? refreshToken;

  final _storage = const FlutterSecureStorage();

  ApiService() {
    _dio.options.baseUrl = ApiConstants.baseUrl;
    // 初始化tokenDio的基础配置,但不添加拦截器
    tokenDio.options = BaseOptions(
      baseUrl: ApiConstants.baseUrl,
      contentType: Headers.jsonContentType,
    );
    // 初始化时同步读取Token(用await确保先获取到Token再处理请求)
    _initTokens();
    _dio.interceptors.add(
      QueuedInterceptorsWrapper(
        onError: (DioError error, ErrorInterceptorHandler handler) async {
          if (error.response?.statusCode == 401 && _retryCount < _maxRetryCount) {
            log("Hata Aldık! ${error.requestOptions.path}");
            final RequestOptions options = error.response!.requestOptions;

            // 检查当前请求是否已经用了最新Token,避免重复刷新
            if ("Bearer $accessToken" == options.headers[HttpHeaders.authorizationHeader]) {
              _retryCount++;
              try {
                // 用不带拦截器的tokenDio发起刷新请求,避免触发拦截器循环
                final response = await tokenDio.get(
                  "/Auth/RefreshTokenLogin?refreshToken=${Uri.encodeQueryComponent(refreshToken!)}",
                );

                if (response.statusCode == 200) {
                  final RefreshTokenResponseModel? model = RefreshTokenResponseModel.fromJson(response.data);
                  if (model?.data == null) {
                    throw Exception("Token refresh data is null");
                  }

                  // 更新本地Token存储
                  await _storage.write(key: "token", value: model!.data!.token);
                  await _storage.write(key: "refreshToken", value: model.data!.refreshToken);
                  refreshToken = model.data!.refreshToken;
                  accessToken = model.data!.token;

                  // 更新请求头的Token
                  options.headers[HttpHeaders.authorizationHeader] = "Bearer ${model.data!.token}";
                  // 重置重试次数
                  _retryCount = 0;

                  // 处理合约通知逻辑
                  if (model.data!.contractsToBeApproved!.isNotEmpty) {
                    final AuthController authController = Get.find();
                    authController.contractsToBeApproved.value = model.data!.contractsToBeApproved!;
                  }

                  // 重新发起原请求
                  final retryResponse = await _dio.fetch(options);
                  handler.resolve(retryResponse);
                  return;
                } else {
                  // 刷新失败,退出登录或处理其他逻辑
                  _handleTokenRefreshFailure();
                  handler.reject(error);
                }
              } catch (e) {
                log("Token refresh failed: $e");
                _handleTokenRefreshFailure();
                handler.reject(error);
              }
            } else {
              // 当前请求的Token不是最新的,直接用最新Token重试
              options.headers[HttpHeaders.authorizationHeader] = "Bearer $accessToken";
              final retryResponse = await _dio.fetch(options);
              handler.resolve(retryResponse);
              return;
            }
          }
          // 超过重试次数或非401错误,直接传递错误
          _retryCount = 0;
          handler.next(error);
        },
        onRequest: (RequestOptions options, RequestInterceptorHandler handler) async {
          log("istek atıldı.  ${options.path}");

          // 如果Token还未初始化,等待初始化完成
          if (accessToken == null) {
            await _initTokens();
          }
          // 设置最新的Token到请求头
          options.headers[HttpHeaders.authorizationHeader] = "Bearer $accessToken";
          handler.next(options);
        },
      ),
    );
  }

  // 初始化Token的异步方法,确保Token加载完成
  Future<void> _initTokens() async {
    accessToken ??= await _storage.read(key: "token");
    refreshToken ??= await _storage.read(key: "refreshToken");
  }

  // Token刷新失败后的处理逻辑(比如退出登录、清空存储)
  void _handleTokenRefreshFailure() async {
    await _storage.delete(key: "token");
    await _storage.delete(key: "refreshToken");
    accessToken = null;
    refreshToken = null;
    // 这里可以添加跳转到登录页的逻辑
    // Get.offAll(() => LoginPage());
  }
}

关键修改点说明

  • 用tokenDio发起刷新请求:避免带拦截器的_dio触发401拦截逻辑,打破循环。
  • 添加重试次数限制:设置_maxRetryCount,防止无限重试。
  • 异步操作改用await:确保Token更新完成后再发起重试请求,避免状态不一致。
  • 统一Token初始化逻辑:用_initTokens()方法确保Token加载完成后再处理请求。
  • 增加刷新失败处理:当刷新Token失败时,清空本地存储并引导用户重新登录,终止循环。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.19 23:10:28