背景
网上查了一圈,找到一位大佬的文章,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
大佬。