用于刷新访问令牌的 Dio 拦截器

Nei*_*ard 11 flutter dio

我正在尝试使用 Dio 拦截器实现访问令牌刷新。我查看了我能找到的示例,但似乎都不起作用。

这是我的尝试:

class AuthInterceptor extends QueuedInterceptor {
  final Dio dio;
  final AuthService authService;

  AuthInterceptor(this.dio, this.authService);

  @override
  void onRequest(
      RequestOptions options, RequestInterceptorHandler handler) async {
    var accessToken = await TokenRepository.getAccessToken();

    if (accessToken != null) {
      logDebug('Access-Token: $accessToken');
      options.headers['Authorization'] = 'Bearer $accessToken';
    }
    return handler.next(options);
  }

  @override
  void onError(DioError err, ErrorInterceptorHandler handler) async {
    if (err.type == DioErrorType.response && err.response?.statusCode == 401) {
      var accessToken = await TokenRepository.getAccessToken();

      if (accessToken != null) {
        final refreshToken = await TokenRepository.getRefreshToken();
        final refreshResult = await authService
            .refresh(RefreshRequest(accessToken, refreshToken!));
        await TokenRepository.setAccessToken(refreshResult.accessToken);
        handler.resolve(await _retry(err.requestOptions));
      } else {
        di<NavigationHelper>().navigateClearHistory(LoginPage());
        handler.reject(err);
      }
    } else {
      handler.next(err);
    }
  }

  Future&lt;Response&lt;dynamic&gt;&gt; _retry(RequestOptions requestOptions) async {
    final options = Options(
      method: requestOptions.method,
      headers: requestOptions.headers,
    );
    return dio.request&lt;dynamic&gt;(requestOptions.path,
        data: requestOptions.data,
        queryParameters: requestOptions.queryParameters,
        options: options);
  }
}
Run Code Online (Sandbox Code Playgroud)

确实有效,我确实获得了一个新的访问令牌。但是我结束 onError 覆盖的方式有问题。我认为处理程序没有正确完成?我认为 handler.resolve 是错误的,但是我应该如何实现呢?如何正确完成此处理程序并使用新的访问令牌执行重试调用?

gen*_*ser 20

虽然关于如何使用 Dio 拦截器在过期时获取新令牌的信息不多,但我会尽力帮助您。

什么是 HttpClient 拦截器?

HttpClient 拦截器旨在修改、跟踪和验证来自服务器的 HTTP 请求和响应。

从方案中可以看出,拦截器是客户端的一部分,是我们应用程序中最边缘的功能。

在此输入图像描述

使用拦截器进行令牌管理

由于拦截器是向服务器发送 HTTP 请求的最后一部分,因此它是处理请求重试并在过期时获取新令牌的好地方。

您的尝试有问题

让我们谈谈您的代码,并尝试破坏它的每个部分。

根据要求:

虽然这部分应该可以正常工作,但await在每个 HTTP 请求上获取访问令牌的效率很低。它会大大减慢您的应用程序以及您检索 HTTP 响应的持续时间。

我建议您创建一个loadAccessToken()函数,负责将令牌加载到缓存存储库,并在每个请求中使用该缓存令牌。

请注意,我不知道你的TokenRepository.getAccessToken()函数在幕后做什么。但如果是 HTTP 请求,请注意它也会被拦截!如果您无法访问服务器,您将陷入尝试获取令牌的无限循环。

正如我稍后将解释的,我使用flutter_secure_storage包在应用程序中安全地保存新令牌,同时在第一个 HTTP 请求上加载该令牌以进行缓存。

错误:

我建议您使用jwt_decode包,在将过期令牌发送到服务器(onRequest)之前识别它们。这样,您将仅在过期时检索新令牌,并且每个 HTTP 请求将仅使用经过验证的令牌发送。

感谢 @FDuhen 评论,值得一提的是识别和处理 401 响应很重要(因为并非所有 401 都与令牌过期相关)。您可以通过全局行为的拦截器(例如将用户重定向回登录流程)或每个请求(并从您的 处理它ApiRepository)来执行此操作。为了示例的简单性,我不会重写onErrorInterceptor 函数。

我的方法

现在让我们看一下我的代码。它并不完美,但希望它能帮助您更好地理解如何使用拦截器处理令牌。

首先,我创建了一个loadAccessToken()函数作为我的tokenRepository类的一部分,其目的是将令牌武装到缓存配置存储库中。

该函数的工作原理分为三个步骤:

  1. 检查令牌是否在缓存配置中。
  2. 如果没有,则尝试从本地存储获取令牌(使用flutter_secure_storage)。
  3. 如果本地不存在,则尝试从服务器获取该令牌。

在每个步骤 (1, 2) 中,我使用jwt_decoder包验证令牌未过期。如果它过期了,那么我从服务器获取它。

每次从服务器获取新令牌时,都需要使用flutter_secure_storage将其保存在本地(我们不想使用 shared_preferences 保存它,因为它不安全)。这样,您的令牌将被安全保存,并且检索速度会很快。

我的loadAccessToken功能:

/// Tries to get accessToken from [AppConfig], localSecureStorage or Keycloak
  /// servers, and update them if necessary
  Future<String?> get loadAccessToken async {
    // get token from cache
    var accessToken = _config.accessToken;
    if (accessToken != null && !tokenHasExpired(accessToken)) {
      return accessToken;
    }
    // get token from secure storage
    accessToken =
        await LocalSecureStorageRepository.get(SecureStorageKeys.accessToken);
    if (accessToken != null && !tokenHasExpired(accessToken)) {
      // update cache
      _config.accessToken = accessToken;
      return accessToken;
    }
    // get token from Keycloak server
    final keycloakTokenResponse = await _accessTokenFromKeycloakServer;
    accessToken = keycloakTokenResponse.accessToken;
    final refreshToken = keycloakTokenResponse.refreshToken;
    if (!tokenHasExpired(accessToken) && !tokenHasExpired(refreshToken)) {
      // update secure storage
      await Future.wait([
        LocalSecureStorageRepository.update(
          SecureStorageKeys.accessToken,
          accessToken,
        ),
        LocalSecureStorageRepository.update(
          SecureStorageKeys.refreshToken,
          refreshToken,
        )
      ]);
      // update cache
      _config.accessToken = accessToken;
      return accessToken;
    }
    return null;
  }
Run Code Online (Sandbox Code Playgroud)

这是tokenHasExpired函数(使用jwt_decoder包):

bool tokenHasExpired(String? token) {
    if (token == null) return true;
    return Jwt.isExpired(token);
  }
Run Code Online (Sandbox Code Playgroud)

现在,使用我们的拦截器处理访问令牌将变得更加容易。正如您在下面看到的(在我的拦截器示例中),我传递了一个单例AppConfig实例和一个tokenRepository包含loadAccessToken()我们之前讨论过的函数的实例。

我在覆盖功能上所做的onRequest就是

  1. 验证该请求是否应与访问令牌一起使用(与我们的讨论无关)。
  2. 从缓存配置中获取令牌(使用实例AppConfig)。
  3. 如果缓存中不存在该令牌(可能是应用程序打开中的第一个 HTTP 请求),或者缓存的令牌已过期,则在这两种情况下都使用该函数加载新的访问令牌loadAccessToken(首先从存储中加载,然后才从服务器令牌提供者加载) 。

我的拦截器:

class ApiProviderTokenInterceptor extends Interceptor {
  ApiProviderTokenInterceptor(this._config, this._tokenRepository);

  final AppConfig _config;
  final TokenRepository _tokenRepository;

  @override
  Future<void> onRequest(
    RequestOptions options,
    RequestInterceptorHandler handler,
  ) async {
    if (options.headers['requires-token'] == 'false') {
      // if the request doesn't need token, then just continue to the next
      // interceptor
      options.headers.remove('requiresToken'); //remove the auxiliary header
      return handler.next(options);
    }

    var token = _config.accessToken;
    if (token == null || _tokenRepository.tokenHasExpired(token)) {
      token = await _tokenRepository.loadAccessToken;
    }

    options.headers.addAll({'authorization': 'Bearer ${token!}'});
    return handler.next(options);
  }

  @override
  void onResponse(
    Response<dynamic> response,
    ResponseInterceptorHandler handler,
  ) {
    return handler.next(response);
  }

  @override
  void onError(DioError err, ErrorInterceptorHandler handler) {
    // <-- here you can handle 401 response, which is not related to token expiration, globally to all requests
    return handler.next(err);
  }
}
Run Code Online (Sandbox Code Playgroud)

如果您在从服务器(在函数上)检索新令牌时遇到错误loadAccessToken()。然后处理错误tokenRepository并决定向您的客户呈现什么(一般性的内容,例如“我们的服务器有问题,请稍后再试”,应该没问题)。

奖金

您可以添加内置的 DioLogInterceptor来打印每个请求和响应(对调试非常有帮助)。

或者您可以使用Pretty_dio_logger包来打印漂亮的彩色请求和响应日志。

_ApiProvider(
        dio
          ..interceptors.add(authInterceptor)
          // ..interceptors.add(LogInterceptor())
          ..interceptors.add(
            PrettyDioLogger(
              requestBody: true,
              requestHeader: true,
            ),
          ),
      ); 
Run Code Online (Sandbox Code Playgroud)

  • 感谢您的努力 (3认同)

shi*_*kla 0

我发现了一个这样的例子,我可以使用 Dio 拦截器刷新访问令牌。

class DioHelper {
  final Dio dio;
  DioHelper({@required this.dio});

  final CustomSharedPreferences _customSharedPreferences =
      new CustomSharedPreferences();
  static String _baseUrl = BASE_URL;
  String token = "";
  
  void initializeToken(String savedToken) {
    token = savedToken;
    _initApiClient();
  }

  Future<void> initApiClient() {
    dio.interceptors
        .add(InterceptorsWrapper(onRequest: (RequestOptions options) {
      options.headers["Authorization"] = "Bearer " + token;
      return options;
    }, onResponse: (Response response) {
      return response;
    }, onError: (DioError error) async {
      RequestOptions origin = error.response.request;
      if (error.response.statusCode == 401) {
        try {
          Response<dynamic> data = await dio.get("your_refresh_url");
          token = data.data['newToken'];
          _customSharedPreferences.setToken(data.data['newToken']);
          origin.headers["Authorization"] = "Bearer " + data.data['newToken'];
          return dio.request(origin.path, options: origin);
        } catch (err) {
          return err;
        }
      }
      return error;
    }));
    dio.options.baseUrl = _baseUrl;
  }

  Future<dynamic> get(String url) async {
    try {
      final response = await dio.get(url);
      var apiResponse = ApiResponse.fromJson(response.data);
      if (apiResponse.status != 200) {
        throw Exception(apiResponse.message);
      }
      return apiResponse.data;
    } on DioError catch (e) {
      // debugging purpose
      print('[Dio Helper - GET] Connection Exception => ' + e.message);
      throw e;
    }
  }

  Future<dynamic> post(String url,
      {Map headers, @required data, encoding}) async {
    try {
      final response =
          await dio.post(url, data: data, options: Options(headers: headers));
      ApiResponse apiResponse = ApiResponse.fromJson(response.data);
      if (apiResponse.status != 200) {
        throw Exception(apiResponse.message);
      }
      return apiResponse.data;
    } on DioError catch (e) {
      // debugging purpose
      print('[Dio Helper - GET] Connection Exception => ' + e.message);
      throw e;
    }
  }
}
Run Code Online (Sandbox Code Playgroud)

你能调查一下吗