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

相关推荐
helloxmg5 小时前
鸿蒙harmonyos next flutter通信之MethodChannel获取设备信息
flutter
helloxmg5 小时前
鸿蒙harmonyos next flutter混合开发之开发package
flutter·华为·harmonyos
lqj_本人1 天前
flutter_鸿蒙next_Dart基础②List
flutter
lqj_本人1 天前
flutter_鸿蒙next_Dart基础①字符串
flutter
The_tuber_sadness1 天前
【Flutter】- 基础语法
flutter
helloxmg1 天前
鸿蒙harmonyos next flutter通信之BasicMessageChannel获取app版本号
flutter
linpengteng2 天前
使用 Flutter 开发数字钱包应用(Dompet App)
前端·flutter·firebase
云兮Coder2 天前
鸿蒙 HarmonyNext 与 Flutter 的异同之处
flutter·华为·harmonyos