我正在尝试创建一个自定义ApiClient
类,我可以将其作为依赖项(与get_it
包一起)注入到应用程序的数据层中。为了避免在我的应用程序的表示/应用程序/域层中担心访问令牌,我希望有一个字段accessToken
,它可以跟踪ApiClient
(单例)类中的accessToken
。
整个数据层都将使用ApiClient
类来处理对服务器的数据请求。它应该有一种方法,允许我将自己的请求添加到它中,以获得唯一的路由。然后,如果这些路由需要访问令牌,它将与请求一起从类中添加accessToken
字段。如果该访问令牌无效(过期/篡改),则将使用设备存储中的刷新令牌,并向服务器发送请求以获得新的访问令牌,然后再次尝试原始请求。它最多会“重试”请求一次。然后,如果仍然存在错误,它只返回要处理的内容。
我真的在为如何实现这一点而苦苦挣扎。我目前的尝试是在下面。任何帮助都会是惊人的!
class ApiClient {
final String baseUrl;
final Dio dio;
final NetworkInfo networkInfo;
final FlutterSecureStorage secureStorage;
ApiClient(
{required this.baseUrl,
required this.dio,
required this.networkInfo,
required this.secureStorage}) {
dio.interceptors.add(RefreshInvalidTokenInterceptor(networkInfo, dio, secureStorage));
}
}
class RefreshInvalidTokenInterceptor extends QueuedInterceptor {
final NetworkInfo networkInfo;
final Dio dio;
final FlutterSecureStorage secureStorage;
String? accessToken;
RefreshInvalidTokenInterceptor(this.networkInfo, this.dio, this.secureStorage);
@override
Future onError(DioError err, ErrorInterceptorHandler handler) async {
if (_shouldRetry(err) && await networkInfo.isConnected) {
try {
// access token request (using refresh token from flutter_secure_storage)
final refreshToken = await secureStorage.read(key: "refreshToken");
final response = await dio.post(
"$kDomain/api/user/token",
queryParameters: {"token": refreshToken},
);
accessToken = response.data["accessToken"];
return err;
} on DioError catch (e) {
handler.next(e);
} catch (e) {
handler.next(err);
}
} else {
handler.next(err);
}
}
bool _shouldRetry(DioError err) =>
(err.response!.statusCode == 403 || err.response!.statusCode == 401);
}
网上也有类似的问题,但似乎没有人回答我的问题!:)
编辑:我得到了一个可行的解决方案(几乎),只有一个错误。这是可行的(除了函数retryRequest()
之外,我正在硬编码请求为post请求):
<imports removed for simplicity>
class ApiClient {
final Dio dio;
final NetworkInfo networkInfo;
final FlutterSecureStorage secureStorage;
String? accessToken;
ApiClient({
required this.dio,
required this.networkInfo,
required this.secureStorage,
}) {
dio.options = BaseOptions(
connectTimeout: 5000,
receiveTimeout: 3000,
receiveDataWhenStatusError: true,
followRedirects: true,
headers: {"content-Type": "application/json"},
);
dio.interceptors.add(QueuedInterceptorsWrapper(
//! ON REQUEST
onRequest: (options, handler) {
handler.next(options);
},
//! ON RESPONSE
onResponse: (response, handler) {
print("onResponse...");
handler.next(response);
},
//! ON ERROR
onError: (error, handler) async {
print("onError...");
if (tokenInvalid(error)) {
print("token invalid: retrying");
print("header before: ${dio.options.headers}");
await getAccessTokenAndSetToHeader(dio);
print("header after: ${dio.options.headers}");
final response = await retryRequest(error, handler);
handler.resolve(response);
print("here-1");
} else {
handler.reject(error);
}
print("here-2");
print("here-3");
},
));
}
Future<String?> getRefreshToken() async => await secureStorage.read(key: "refreshToken");
Future<void> getAccessTokenAndSetToHeader(Dio dio) async {
final refreshToken = await secureStorage.read(key: "refreshToken");
if (refreshToken == null || refreshToken.isEmpty) {
print("NO REFRESH TOKEN ERROR; LOGOUT!!!");
throw ServerException();
} else {
final response = await dio.post(
"$kDomain/api/user/token",
data: {"token": refreshToken},
);
dio.options.headers["authorization"] = "Bearer ${response.data["accessToken"]}";
}
}
// This function has the hardcoded post
Future<Response> retryRequest(DioError error, ErrorInterceptorHandler handler) async {
print("retry called, headers: ${dio.options.headers}");
final retryResponse = await dio.post(error.requestOptions.path);
print("retry results: $retryResponse");
return retryResponse;
}
bool tokenInvalid(DioError error) =>
error.response?.statusCode == 403 || error.response?.statusCode == 401;
Future<void> refreshToken() async {}
bool validStatusCode(Response response) =>
response.statusCode == 200 || response.statusCode == 201;
}
但是,如果我将硬编码post请求更改为:
final retryResponse =
await dio.request(error.requestOptions.path, data: error.requestOptions.data);
密码不再有效了..。有人知道为什么吗?让它基于失败的请求是动态的,让我重复使用这个类。
发布于 2022-08-01 04:11:54
package:dio
已经包含了BaseOptions
,您可以使用它来添加一些基本配置,比如baseUrl.
之后,您可以使用拦截器将accessToken
添加到每个请求中。要做到这一点,取决于您的状态管理解决方案,您可以在用户身份验证状态更改时更新accessToken
。
最后,关于令牌刷新,您可以签出package:fresh_dio
。
发布于 2022-08-01 11:10:44
想出了!(代码+如何使用下面的代码)
下面是我的整个ApiClient
类(为简单起见而隐藏的导入)。它使用迪奥充当HTTP客户端。
class ApiClient {
final Dio dio;
final NetworkInfo networkInfo;
final FlutterSecureStorage secureStorage;
String? accessToken;
/// The base options for all requests with this Dio client.
final BaseOptions baseOptions = BaseOptions(
connectTimeout: 5000,
receiveTimeout: 3000,
receiveDataWhenStatusError: true,
followRedirects: true,
headers: {"content-Type": "application/json"},
baseUrl: kDomain, // Domain constant (base path).
);
/// Is the current access token valid? Checks if it's null, empty, or expired.
bool get validToken {
if (accessToken == null || accessToken!.isEmpty || Jwt.isExpired(accessToken!)) return false;
return true;
}
ApiClient({
required this.dio,
required this.networkInfo,
required this.secureStorage,
}) {
dio.options = baseOptions;
dio.interceptors.add(QueuedInterceptorsWrapper(
// Runs before a request happens. If there's no valid access token, it'll
// get a new one before running the request.
onRequest: (options, handler) async {
if (!validToken) {
await getAndSetAccessTokenVariable(dio);
}
setHeader(options);
handler.next(options);
},
// Runs on an error. If this error is a token error (401 or 403), then the access token
// is refreshed and the request is re-run.
onError: (error, handler) async {
if (tokenInvalidResponse(error)) {
await refreshAndRedoRequest(error, handler);
} else {
// Other error occurs (non-token issue).
handler.reject(error);
}
},
));
}
/// Sets the current [accessToken] to request header.
void setHeader(RequestOptions options) =>
options.headers["authorization"] = "Bearer $accessToken";
/// Refreshes access token, sets it to header, and resolves cloned request of the original.
Future<void> refreshAndRedoRequest(DioError error, ErrorInterceptorHandler handler) async {
await getAndSetAccessTokenVariable(dio);
setHeader(error.requestOptions);
handler.resolve(await dio.post(error.requestOptions.path,
data: error.requestOptions.data, options: Options(method: error.requestOptions.method)));
}
/// Gets new access token using the device's refresh token and sets it to [accessToken] class field.
///
/// If the refresh token from the device's storage is null or empty, an [EmptyTokenException] is thrown.
/// This should be handled with care. This means the user has somehow been logged out!
Future<void> getAndSetAccessTokenVariable(Dio dio) async {
final refreshToken = await secureStorage.read(key: "refreshToken");
if (refreshToken == null || refreshToken.isEmpty) {
// User is no longer logged in!
throw EmptyTokenException();
} else {
// New DIO instance so it doesn't get blocked by QueuedInterceptorsWrapper.
// Refreshes token from endpoint.
try {
final response = await Dio(baseOptions).post(
"/api/user/token",
data: {"token": refreshToken},
);
// If refresh fails, throw a custom exception.
if (!validStatusCode(response)) {
throw ServerException();
}
accessToken = response.data["accessToken"];
} on DioError catch (e) {
// Based on the different dio errors, throw custom exception classes.
switch (e.type) {
case DioErrorType.sendTimeout:
throw ConnectionException();
case DioErrorType.connectTimeout:
throw ConnectionException();
case DioErrorType.receiveTimeout:
throw ConnectionException();
case DioErrorType.response:
throw ServerException();
default:
throw ServerException();
}
}
}
}
bool tokenInvalidResponse(DioError error) =>
error.response?.statusCode == 403 || error.response?.statusCode == 401;
bool validStatusCode(Response response) =>
response.statusCode == 200 || response.statusCode == 201;
}
应该将它作为单例注入到您的项目中,因此只有一个实例(为了保持其accessToken
字段的状态)。我是这样使用它的:
// Registers the custom ApiClient class.
sl.registerLazySingleton(() => ApiClient(dio: sl(), networkInfo: sl(), secureStorage: sl()));
然后,在数据层(或从何处调用API)中,可以通过构造函数传递它:
class MyDatasource implements IMyDatasource {
final ApiClient apiClient;
late Dio api;
FeedDatasource({required this.client, required this.apiClient}) {
api = apiClient.dio;
}
// Logic for your class here.
}
我将其简化为api
,这样就不必每次调用apiClient.dio...
(可选)。
然后,您可以在类的一个方法中使用它,如下所示:
@override
Future<List<SomeData>> fetchSomeDataFromApi() async {
try {
final response = await api.post("/api/data/whatYouWant");
throw ServerException();
} catch (e) {
throw ServerException();
}
}
现在,对于这个请求,如果您的类有一个有效的访问令牌(非空、非空、非过期),它将正常调用。但是,如果令牌无效,它将首先刷新它,然后继续您的调用。即使在令牌最初通过验证检查(例如令牌在调用期间过期)之后调用失败,它仍将被刷新,并且调用将被重新执行。
注意:我使用了很多自定义异常,这是可选的。
希望这能帮到别人!
https://stackoverflow.com/questions/73182749
复制相似问题