flutetr dio 拦截器实现 token 失效刷新(解决并发)

背景

网上查了一圈,找到一位大佬的文章,flutetr dio 拦截器实现 token 失效刷新。拜读应用后,发现一个致命场景。即一个业务中并发请求多个接口,这多个请求都是401(Token过期),最快返回的触发Refresh Token,后续返回的也触发Refresh Token。连续Refresh Token会导致刷新Token接口报错直接退出。出现这种场景的主要原因是在dio拦截器加锁之前,拦截器队列中已经进入请求了,即拦截器onError中会连续捕获到。

又在网上查了一圈,一无所获。又在Flutter QQ群里寻找大佬讨论半天,终于有了点思路,趁热打铁。本文章侧重解决并发问题,建议读者先拜读上面大佬的文章理解全貌。

dart 复制代码
dio.interceptors.add(InterceptorsWrapper(onRequest: (options, handler) async {
    if (G.user.data.token != null) {
      String? token = G.user.data.token;
      if (!options.path.contains('/auth/oauth/token')) {;
        options.headers['Authorization'] = 'Bearer ' + token!;
        options.headers['TENANT-ID'] = G.user.data.tenantId;
      } else {}
    }

    handler.next(options);
    // 如果你想完成请求并返回一些自定义数据,可以返回一个`Response`对象或返回`dio.resolve(data)`。
    // 这样请求将会被终止,上层then会被调用,then中返回的数据将是你的自定义数据data.
    //
    // 如果你想终止请求并触发一个错误,你可以返回一个`DioError`对象,或返回`dio.reject(errMsg)`,
    // 这样请求将被中止并触发异常,上层catchError会被调用。
  }, onResponse: (response, handler) async {
    // 在返回响应数据之前做一些预处理
    if (response.data.containsKey('code')) {
      if (response.data['code'] != 0) {
        await G.toast(response.data['msg'] ?? 'API 请求业务处理失败');
        handler.reject(DioError(requestOptions: response.requestOptions));
      } else {
        if (response.data?.containsKey('data')) {
          response.data = response.data['data'];
        }
      }
    }
    handler.resolve(response);
  }, onError: (DioError e, handler) async {
    if (e.response?.statusCode == 401) {
      RequestOptions requestOptions = e.response!.requestOptions;
      String uuid = Uuid().v4();
      _requestOptionsList[uuid] = requestOptions;
      _handlers[uuid] = handler;
      if (requestOptions.headers['CheckToken'] != null) {
        await logout();
      } else {
        if (!_refreshingToken) {
          _refreshingToken = true;
          await refreshToken(e, handler);
        }
      }
    } else if (e.response?.statusCode != 200) {
      // 当请求失败时做一些预处理
      handler.next(e); //continue
      if (!(e.response?.data == null || e.response?.data == '')) {
        var _msg = e.response?.data['msg'];
        G.toast(_msg ?? 'API 请求失败');
      }
    } else {
      // 当请求失败时做一些预处理
      handler.next(e); //continue
    }
  }));

设置接口唯一标识

针对401的接口设置一个唯一标识X-Api-Refresh-Key生成的uuid(标注在请求体headers中),根据唯一标识释放请求回调体队列,避免业务回调错乱。

构建请求体队列

Map<String, RequestOptions> _requestOptionsList = {}

Map<String, RequestOptions> _handlers = {}。

_requestOptionsList负责将拦截器加锁前并发的请求体收集。_handlers负责将拦截器加锁前并发的请求回调体handler收集。Map中key为接口唯一标识。

handler 参数来指定一个 Handler 对象。这个 Handler 对象负责在拦截器加锁前并发地处理请求回调体。

增加开关控制

增加一个全局开关,最快请求异常返回401时发起刷新Token操作,开关打开_refreshingToken(正在刷新token)。其它后续求异常返回401时不发起刷新Token操作。最后在Refresh Token完成/异常后关闭。

dart 复制代码
if (!_refreshingToken) {
  _refreshingToken = true;
  await refreshToken(e, handler);
} else {
  G.print.warnText('---------------------> 正在刷新Token');
}

拦截器加锁/解锁

refreshToken过程中加锁dio.lock,避免请求再进入拦截器。完成/异常后解锁dio.unlock。理解一下官方解释如下图:

拦截器加锁后意味着所有请求都无法正常处理,即刷新Token接口本身也无法下发成功。重新new Dio()实例来单独下发这个请求。

dart 复制代码
Dio tokenDio = Dio(_baseOptions);
dio.lock();
G.req.auth
        .refreshToken(
            tokenDio: tokenDio, refreshToken: G.user.data.refresh_token ?? '', tenantId: error.requestOptions.headers['TENANT-ID'].toString())
        .then((res) async {
     dio.unlock();
 }).catchError((err){
     dio.unlock();
 });

重发401接口

获取到新Token后,我们要重新发送之前队列收集到的请求体,确保业务无感知,确保业务正常 。新Token获取到后,检查请求队列。循环请求体队列重新下发之前401的请求。操作完成后首先释放_requestOptionsList队列。因为请求都是异步的,完成一个请求释放一个handler。

dart 复制代码
  // 新Token获取到后,检查请求队列。循环请求体队列重新下发之前401的请求。操作完成
  if (token != null && token.isNotEmpty && _requestOptionsList.length > 0) {
    _requestOptionsList.forEach((key, requestOptions) {
      requestOptions.headers["Authorization"] = "Bearer $token";
      requestOptions.headers[_xApiRefreshKey] = key;
      dio
          .request(requestOptions.path,
              queryParameters: requestOptions.queryParameters,
              data: requestOptions.data,
              cancelToken: requestOptions.cancelToken,
              onReceiveProgress: requestOptions.onReceiveProgress,
              onSendProgress: requestOptions.onSendProgress,
              options: new Options(
                method: requestOptions.method,
                headers: requestOptions.headers,
                extra: requestOptions.extra,
                contentType: requestOptions.contentType,
                responseType: requestOptions.responseType,
                sendTimeout: requestOptions.sendTimeout,
                receiveTimeout: requestOptions.receiveTimeout,
              ))
          .then((value) {
            if (res.requestOptions.headers[_xApiRefreshKey] != null) {
              String xApiRefreshKey = res.requestOptions.headers[_xApiRefreshKey];
              _handlers[xApiRefreshKey]!.resolve(res);
              _handlers.remove(xApiRefreshKey);
            }
      });
    });
  }
  _requestOptionsList = {};
 _refreshingToken = false;

后记

草草写完,测试力度不够。有场景遗漏或者错误,欢迎指出。再次感谢QQ群788639759中Let's go大佬。

相关推荐
程序员老刘44 分钟前
你的Flutter项目要不要紧急升级3.47.8?
flutter·客户端
Talyra423 小时前
不想开会员,我写了一个记账 App
flutter
2601_949575867 小时前
JSON5 跨端三方库仓颉适配全流程
前端·javascript·flutter
法的空间7 小时前
AI 编程时代,为什么 Dart 分析会拖慢大型项目的开发
android·flutter·ios
恋猫de小郭9 小时前
Flutter 紧急发布 3.47.8 安全升级版本,建议都升级
android·前端·flutter
好的佩奇1 天前
Dart 之模式
android·flutter·dart
淡淡的香烟1 天前
Flutter实现多语言切换
android·flutter·1024程序员节
事圆则缓1 天前
从零搭建一套 Flutter 架构:先跑通数据流
flutter
程序员老刘2 天前
Flutter版本选择指南:3.44稳定了11周正式转正 | 2026年10月
flutter·ai编程·客户端
chicken_h2 天前
【无标题】
flutter·打包