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; } }
关键优化点说明
- 异步等待刷新完成:使用
await确保拿到新Token后再处理请求,避免旧Token导致的401。 - 排除刷新请求的拦截:给刷新请求添加
requiresToken: false标识,防止被拦截器重复处理。 - 请求队列管理:用
_pendingRequests队列存储刷新过程中的请求,刷新完成后批量处理,避免请求丢失或重复刷新。 - 分层错误处理:只有当刷新Token失败时才跳转登录,正常请求的401会尝试刷新后重试,提升用户体验。
内容的提问来源于stack exchange,提问作者miracledev
相关产品推荐
相关产品推荐

