Dio实现无感刷新Token的简易封装

30 阅读2分钟
import 'dart:async';
import 'package:dio/dio.dart';
import 'package:flutter/cupertino.dart';
import 'package:get/get.dart' hide Response;
import 'package:myapp/app/constants/app_values.dart';
import 'package:myapp/app/model/userInfo.dart';
import 'package:myapp/app/network/dio_request_retrier.dart';
import 'package:myapp/app/network/repositories/account_repository_impl.dart';
import 'package:myapp/app/routes/app_pages.dart';
import 'package:myapp/app/service/storage_service.dart';
import 'package:myapp/app/utils/logger/logger.dart';

 
// handler.next(...) 将请求/响应/错误 继续交给下一个拦截器

// handler.resolve(response) 手动返回一个响应结果,终止后续的错误传播

// handler.reject(error) 手动返回一个错误对象(DioException),让请求流程走错误分支

// token过期时跳转到登录页

// QueuedInterceptor,串行等待拦截器

// Token 过期自动刷新拦截器

// - 防止并发重复刷新

// - 失败时自动跳转登录

// - 刷新中其他请求会等待刷新完成再重试

class ErrorInterceptor extends InterceptorsWrapper {

bool _isRedirectingToLogin = false; // 防止重复跳转登录

bool _isRefreshingToken = false; // 当前是否正在刷新 token

Completer<void>? _refreshCompleter; // 用于通知等待的请求

 

@override

void onResponse(Response response, ResponseInterceptorHandler handler) async {

logger.i(response.requestOptions.path);

// // 判断请求路径是否是刷新 token 请求

// 检查自定义返回 code(示例:303 表示 token 过期)

if (response.data is Map<String, dynamic> &&

response.data['code'] == 303 &&

response.data['data']['type'] == "need login") {

// === 如果正在刷新 token ===

if (_isRefreshingToken && _refreshCompleter != null) {

logger.w("已有刷新任务,等待刷新完成后重试请求...");

try {

// 等待刷新结束(成功/失败都会唤醒),

// 表示“刷新操作是否完成”的一个可等待对象

await _refreshCompleter!.future;

} catch (e) {

logger.e("等待刷新任务失败: $e");

return handler.reject(

DioException(

requestOptions: response.requestOptions,

error: "刷新 token 失败(等待阶段): $e",

),

);

}

 

// === 刷新完成,重试当前请求 ===

final retrier = DioRequestRetrier(

requestOptions: response.requestOptions,

);

final retryResponse = await retrier.retry();

return handler.resolve(retryResponse);

}

if (!_isRefreshingToken) {

// === 当前是第一个发现 token 过期的请求 ===

_isRefreshingToken = true;

// Completer 是 Dart 的一个 异步控制器(promise-like 对象)。

// 它能手动决定一个 Future

// 什么时候“完成(complete)”或“失败(completeError)”。

_refreshCompleter = Completer<void>();

logger.w("开始刷新 token...");

 

try {

// TODO: 实际刷新 token 逻辑(示例)

/*

final refreshToken = await SecureStorageService.instance.getString(AppValues.refreshToken);

final refreshResponse = await DioProvider.tokenClient.post(

'/auth/refresh',

data: {'refresh_token': refreshToken},

);

final newAccessToken = refreshResponse.data['access_token'];

await SecureStorageService.instance.setString(AppValues.idToken, newAccessToken);

*/

// final accountRepo = AccountRepositoryImpl();

// try {

// // 调用登录接口

// // UserInfo user = await accountRepo.login("13085314047", "123456", "86");

// UserInfo user = await accountRepo.login(

// "13085314047",

// "123456",

// "86",

// );

// // 登录成功后处理用户信息

// final storage = SecureStorageService.instance;

// // 可以存储 token

// await storage.setAsyncString(AppValues.idToken, user.token ?? "");

// } catch (e) {

// // 统一错误处理

// // 这里捕获 BaseRepository 里抛出的各种异常(网络错误、接口错误、解析错误)

// logger.i("登录失败: $e");

// //ToastUtil.showWarning("登录失败: $e");

// }

// [表情] 刷新成功

if (!(_refreshCompleter?.isCompleted ?? true)) {

_refreshCompleter!.complete(); // 表示刷新成功

}

_isRefreshingToken = false;

 

// 重试当前请求

final retrier = DioRequestRetrier(

requestOptions: response.requestOptions,

);

final retryResponse = await retrier.retry();

return handler.resolve(retryResponse);

} catch (e) {

logger.e("刷新 token 失败: $e");

// [表情] 刷新失败:通知等待请求并跳转登录

if (!(_refreshCompleter?.isCompleted ?? true)) {

_refreshCompleter!.completeError(e); // 表示刷新失败

}

 

_isRefreshingToken = false;

 

// 跳转登录页(防止多次跳转)

if (!_isRedirectingToLogin && Get.currentRoute != Routes.LOGIN) {

_isRedirectingToLogin = true;

Future.microtask(() {

Get.offAllNamed(Routes.LOGIN);

_isRedirectingToLogin = false;

});

}

 

return handler.reject(

DioException(

requestOptions: response.requestOptions,

error: "刷新 token 失败",

),

);

} finally {

// 防止状态残留导致死锁

_isRefreshingToken = false;

_refreshCompleter = null;

}

}

}

 

// 非 token 过期情况直接放行

super.onResponse(response, handler);

}

 

@override

void onError(DioException e, ErrorInterceptorHandler handler) {

if (e.response?.statusCode == 401) {

// Token 失效跳转到登录页

// 防止重复跳转,并且确保当前不是登录页

if (!_isRedirectingToLogin && Get.currentRoute != Routes.LOGIN) {

_isRedirectingToLogin = true;

// 延迟到微任务,避免 build/route 冲突

// 它会 延后当前同步代码执行完成后再执行回调,保证:

// 当前函数或拦截器中的逻辑先执行完

// Flutter 的路由和上下文已经准备好

Future.microtask(() {

Get.offAllNamed(Routes.LOGIN);

_isRedirectingToLogin = false;

});

}

/*

// Token 过期,使用 DioRequestRetrier 重试请求

try {

final retrier = DioRequestRetrier(requestOptions: e.requestOptions);

final response = await retrier.retry();

return handler.resolve(response);

} catch (retryError) {

debugPrint("Token 刷新失败: $retryError");

return handler.reject(e);

}

*/

super.onError(e, handler);

} else if (e.type == DioExceptionType.unknown) {

// 处理网络错误

debugPrint("网络错误: ${e.message}");

// TODO: 添加网络错误提示

handler.reject(e);

} else {

// 其他错误继续传递

// 在 Dio 的默认实现里,super.onError()

// 的内部实现就是 handler.next(e)

// handler.next(e);

super.onError(e, handler);

}

}

}