Flutter Dio拦截器问题:401响应处理与AccessToken刷新异常
Flutter Dio拦截器问题:Token更新不生效与401登出逻辑优化
问题分析
你的代码存在两个核心问题:
- Token更新后部分请求仍使用旧令牌:大概率是因为
DioClient未采用单例模式,多个实例各自维护独立的拦截器和Token状态,导致Token更新无法全局生效;同时onResponse中修改response.requestOptions.headers的操作对后续请求无意义,属于冗余逻辑。 - 401登出时机错误:当前只要收到401就直接登出,未区分"access token过期"和"refresh token过期"的场景,应该优先尝试用refresh token刷新access token,仅当刷新失败时才触发登出。
解决方案
1. 确保Token更新全局生效
步骤1:将DioClient改为单例模式
保证整个应用只有一个Dio实例和拦截器,所有请求共享同一套Token逻辑:
class DioClient { // 单例实现 static final DioClient _instance = DioClient._internal(); factory DioClient() => _instance; Dio dio = Dio(BaseOptions( connectTimeout: const Duration(seconds: 10), receiveTimeout: const Duration(seconds: 100), receiveDataWhenStatusError: true, followRedirects: true, headers: {"Content-Type": 'application/json'}, )); TokenManager tokenManager = TokenManager(); AppPreference appPreference = AppPreference(); bool _isRefreshing = false; final _refreshCompleter = Completer<void>(); bool _isDialogShown = false; DioClient._internal() { _setupInterceptors(); } void _setupInterceptors() { dio.interceptors.clear(); dio.interceptors.add(InterceptorsWrapper( onRequest: (options, handler) async { // 等待Token刷新完成再发起请求,避免并发请求使用旧Token if (_isRefreshing) { await _refreshCompleter.future; } final accessToken = await tokenManager.getAccessToken(); final refreshToken = await tokenManager.getRefreshToken(); if (accessToken != null) { options.headers['Authorization'] = 'Bearer $accessToken'; } if (refreshToken != null) { options.headers['Cookie'] = refreshToken; } handler.next(options); }, onResponse: (response, handler) async { // 仅更新TokenManager中的Token,后续请求自动读取最新值 if (response.statusCode == 200 && response.data != null) { Map<String, dynamic> responseData = response.data; if (responseData.containsKey("newAccessToken") && responseData["newAccessToken"] != null) { final newAccessToken = responseData["newAccessToken"]; await tokenManager.writeAccessToken(newAccessToken); } } handler.next(response); }, // 401处理逻辑见下文 onError: (DioException error, handler) async { // ... }, )); } }
步骤2:检查TokenManager实现
确保writeAccessToken和getAccessToken正确读写持久化存储(如SharedPreferences、Hive),无异步读写不一致的问题。
2. 优化401登出逻辑
修改onError拦截器,先尝试刷新Token,仅当刷新失败时才执行登出,同时确保拦截器提前清除:
onError: (DioException error, handler) async { if (error.response?.statusCode == 401) { final requestOptions = error.response!.requestOptions; // 排除刷新Token本身的请求,避免死循环 if (!requestOptions.path.contains('/refresh-token')) { if (!_isRefreshing) { _isRefreshing = true; try { final refreshToken = await tokenManager.getRefreshToken(); // 调用后端刷新Token接口 final refreshResponse = await dio.post( '/auth/refresh-token', options: Options(headers: {'Cookie': refreshToken}), ); if (refreshResponse.statusCode == 200) { final newAccessToken = refreshResponse.data['newAccessToken']; final newRefreshToken = refreshResponse.data['newRefreshToken']; await tokenManager.writeAccessToken(newAccessToken); if (newRefreshToken != null) { await tokenManager.writeRefreshToken(newRefreshToken); } // 通知等待的请求刷新完成 _refreshCompleter.complete(); _refreshCompleter = Completer<void>(); _isRefreshing = false; // 重试原请求 requestOptions.headers['Authorization'] = 'Bearer $newAccessToken'; requestOptions.headers['Cookie'] = newRefreshToken ?? refreshToken; return handler.resolve(await dio.fetch(requestOptions)); } else { // 刷新失败,执行登出 await _performLogout(error, handler); } } catch (e) { // 刷新过程出错,执行登出 await _performLogout(error, handler); } finally { _isRefreshing = false; if (!_isRefreshing && !_refreshCompleter.isCompleted) { _refreshCompleter.completeError('Refresh failed'); } } } else { // 等待刷新完成后重试 await _refreshCompleter.future; requestOptions.headers['Authorization'] = 'Bearer ${await tokenManager.getAccessToken()}'; requestOptions.headers['Cookie'] = await tokenManager.getRefreshToken(); return handler.resolve(await dio.fetch(requestOptions)); } } else { // 刷新Token的请求返回401,说明refresh token已过期,直接登出 await _performLogout(error, handler); } } return handler.next(error); }, // 封装登出逻辑 Future<void> _performLogout(DioException error, ErrorInterceptorHandler handler) async { // 先清除拦截器,阻止后续请求触发拦截逻辑 dio.interceptors.clear(); await tokenManager.deleteTokens(); Hive.box<User>('userBox').clear(); await appPreference.logOut(); // 避免重复显示弹窗 if (navigatorKey.currentContext != null && !_isDialogShown) { _isDialogShown = true; showSessionExpiredDialog(navigatorKey.currentContext!); WidgetsBinding.instance.addPostFrameCallback((_) { _isDialogShown = false; }); } // 拒绝当前请求 handler.reject(DioException(requestOptions: error.response!.requestOptions)); }
针对疑问的明确解答
疑问1:如何确保替换新AccessToken后,所有拦截器都使用新令牌?
- 单例模式:全局唯一的DioClient保证所有请求共享同一套拦截器和Token状态,
onRequest每次都会从TokenManager读取最新Token。 - 并发请求处理:通过
_isRefreshing标记和Completer让其他请求等待Token刷新完成,避免并发场景下使用旧Token。 - 移除冗余逻辑:删除
onResponse中修改response.requestOptions.headers的代码,该操作对后续请求无影响。
疑问2:如何确保在处理401错误并拒绝请求前,拦截器已被清除?
- 封装登出方法:在
_performLogout中优先执行dio.interceptors.clear(),再执行清除凭证、显示弹窗等操作,保证拦截器在后续逻辑执行前被完全清除。 - 防重复执行:通过
_isRefreshing和_isDialogShown标记,避免多个401请求同时触发登出逻辑,确保拦截器只被清除一次。
内容的提问来源于stack exchange,提问作者Bishal Rumba
相关产品推荐
相关产品推荐

