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大佬。

相关推荐
Geek_Vison8 小时前
保险APP已经用了Flutter,还能接入小程序吗?
flutter·小程序·uni-app·mpaas·小程序容器
FungLeo8 小时前
成为全栈·Flutter App 篇·go_router 与四 Tab App Shell:保留导航状态、深链和登录回跳
flutter·移动开发·成为全栈·go_router·路由设计·深链接
m0_738185829 小时前
Flutter 鸿蒙化实战:screen_protector 适配 OpenHarmony,防截屏/防泄露
flutter·华为·harmonyos·鸿蒙
梦想的颜色14 小时前
【编程实战】AI 时代 APP 开发全栈硬核指南:技术选型 + AI 架构 + 模型落地全维度决策
python·flutter·react native·react.js·ai·桌面应用·milvus
yuanlaile1 天前
2026年全新Flutter 跨平台开发全栈学习资料整理
flutter·getx·鸿蒙 next·flutter开发鸿蒙·flutter入门实战·flutter全栈
恋猫de小郭2 天前
Genkit Dart 1.0 发布,Flutter 原生的 AI Agent 终于完整了
android·前端·flutter
第10086个小白2 天前
flutter桌面端打包方式(Inno Setup Compiler)版本
前端·flutter
陆断枫3 天前
Flutter 3.47 动画引擎盘点:Impeller 全平台,和那些"新特性"
flutter
程序员老刘3 天前
Flet 1.0 思路很好,可惜来晚了
flutter·ai编程·客户端
m0_738185823 天前
Flutter 鸿蒙化实战:qr_code_scanner_plus 适配 OpenHarmony,二维码扫描
flutter·华为·harmonyos·鸿蒙