首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >问答首页 >Dio Client:如果对受保护路由的请求失败(401代码),则刷新令牌,然后重试。努力创造

Dio Client:如果对受保护路由的请求失败(401代码),则刷新令牌,然后重试。努力创造
EN

Stack Overflow用户
提问于 2022-07-31 11:07:09
回答 2查看 743关注 0票数 1

我正在尝试创建一个自定义ApiClient类,我可以将其作为依赖项(与get_it包一起)注入到应用程序的数据层中。为了避免在我的应用程序的表示/应用程序/域层中担心访问令牌,我希望有一个字段accessToken,它可以跟踪ApiClient (单例)类中的accessToken

整个数据层都将使用ApiClient类来处理对服务器的数据请求。它应该有一种方法,允许我将自己的请求添加到它中,以获得唯一的路由。然后,如果这些路由需要访问令牌,它将与请求一起从类中添加accessToken字段。如果该访问令牌无效(过期/篡改),则将使用设备存储中的刷新令牌,并向服务器发送请求以获得新的访问令牌,然后再次尝试原始请求。它最多会“重试”请求一次。然后,如果仍然存在错误,它只返回要处理的内容。

我真的在为如何实现这一点而苦苦挣扎。我目前的尝试是在下面。任何帮助都会是惊人的!

代码语言:javascript
运行
复制
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请求):

代码语言:javascript
运行
复制
<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请求更改为:

代码语言:javascript
运行
复制
final retryResponse =
        await dio.request(error.requestOptions.path, data: error.requestOptions.data);

密码不再有效了..。有人知道为什么吗?让它基于失败的请求是动态的,让我重复使用这个类。

EN

回答 2

Stack Overflow用户

发布于 2022-08-01 04:11:54

package:dio已经包含了BaseOptions,您可以使用它来添加一些基本配置,比如baseUrl.

之后,您可以使用拦截器将accessToken添加到每个请求中。要做到这一点,取决于您的状态管理解决方案,您可以在用户身份验证状态更改时更新accessToken

最后,关于令牌刷新,您可以签出package:fresh_dio

票数 1
EN

Stack Overflow用户

发布于 2022-08-01 11:10:44

想出了!(代码+如何使用下面的代码)

下面是我的整个ApiClient类(为简单起见而隐藏的导入)。它使用迪奥充当HTTP客户端。

代码语言:javascript
运行
复制
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字段的状态)。我是这样使用的:

代码语言:javascript
运行
复制
// Registers the custom ApiClient class.
sl.registerLazySingleton(() => ApiClient(dio: sl(), networkInfo: sl(), secureStorage: sl()));

然后,在数据层(或从何处调用API)中,可以通过构造函数传递它:

代码语言:javascript
运行
复制
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... (可选)。

然后,您可以在类的一个方法中使用它,如下所示:

代码语言:javascript
运行
复制
 @override
  Future<List<SomeData>> fetchSomeDataFromApi() async {
    try {
      final response = await api.post("/api/data/whatYouWant");
      throw ServerException();
    } catch (e) {
      throw ServerException();
    }
  }

现在,对于这个请求,如果您的类有一个有效的访问令牌(非空、非空、非过期),它将正常调用。但是,如果令牌无效,它将首先刷新它,然后继续您的调用。即使在令牌最初通过验证检查(例如令牌在调用期间过期)之后调用失败,它仍将被刷新,并且调用将被重新执行。

注意:我使用了很多自定义异常,这是可选的。

希望这能帮到别人!

票数 0
EN
页面原文内容由Stack Overflow提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://stackoverflow.com/questions/73182749

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档