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
相关产品推荐
相关产品推荐

