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群788639759Let's go大佬。

相关推荐
KKei16382 小时前
Flutter for OpenHarmony 编程技能树APP技术文章
flutter·华为·harmonyos
KKei16382 小时前
Flutter for OpenHarmony 个人财务管理与记账APP
flutter·华为·harmonyos
KKei16383 小时前
Flutter for OpenHarmony 本地音乐播放器APP
flutter·华为·harmonyos
陆业聪3 小时前
两次Flutter全屏白踩坑复盘:Layout的静默失败,以及AI结对编程的认知盲区
flutter·ai编程·大前端·跨端开发
KKei16383 小时前
Flutter for OpenHarmony 外语单词背诵与听力训练APP
flutter·华为·harmonyos
KKei16383 小时前
Flutter for OpenHarmony学习小组组队与打卡APP技术文章
学习·flutter·华为·harmonyos
tangweiguo030519873 小时前
Flutter 集成排查与 APK 瘦身问题解决
flutter
KKei16383 小时前
Flutter for OpenHarmony学术论文管理APP技术文章
flutter·华为·harmonyos
程序员老刘·17 小时前
Perry能取代Flutter吗?跨平台的三种技术路线
flutter·跨平台开发·客户端开发