Dio 拦截器问题:处理 401 响应并刷新访问令牌

问题描述 投票:0回答:1

我在 Flutter 应用程序中遇到了 Dio 包的问题。我已经实现了一个拦截器,为每个请求添加标头,并通过简单地注销来处理 401 响应。但是,实施似乎表现出意外,我正在寻求指导来解决该问题。

这是我的 DioClient 类的简化版本:

class DioClient {
  Dio dio = Dio(BaseOptions(
    connectTimeout: const Duration(seconds: 10),
    receiveTimeout: const Duration(seconds: 100),
    receiveDataWhenStatusError: true,
    followRedirects: true,
    headers: {"Content-Type": 'application/json'},
  ));

  TokenManager tokenManager = TokenManager();
  AppPreference appPreference = AppPreference();

  DioClient() {
    dio.interceptors
      ..clear()
      ..add(InterceptorsWrapper(
        onRequest: (options, handler) async {
          final accessToken = await tokenManager.getAccessToken();
          final refreshToken = await tokenManager.getRefreshToken();
          options.headers['Authorization'] = 'Bearer $accessToken';
          options.headers['Cookie'] = '$refreshToken';
          handler.next(options);
        },
        onError: (DioException error, handler) async {
          if (error.response?.statusCode == 401) {
            dio.interceptors.clear();
            final options = error.response!.requestOptions;
            await tokenManager.deleteTokens();
            Hive.box<User>('userBox').clear();
            await appPreference.logOut();
            showSessionExpiredDialog(navigatorKey.currentContext!);
            return handler.reject(DioException(requestOptions: options));
          }
          return handler.next(error);
        },
        onResponse: (response, handler) async {
          if (response.statusCode == 200 && response.data != null) {
            Map<String, dynamic> responseData = response.data;
            if (responseData.containsKey("newAccessToken")) {
              final newAccessToken = responseData["newAccessToken"];
              if (newAccessToken != null) {
                await tokenManager.writeAccessToken(newAccessToken);
                response.requestOptions.headers['Authorization'] =
                    'Bearer $newAccessToken';
                final refreshToken = await tokenManager.getRefreshToken();
                response.requestOptions.headers['Cookie'] = '$refreshToken';
                return handler.resolve(response);
              }
            }
          }
          return handler.next(response);
        },
      ));
  }
}

void showSessionExpiredDialog(BuildContext context) {
  showDialog(
    barrierDismissible: false,
    context: context,
    builder: (BuildContext context) {
      return AlertDialog(
        alignment: Alignment.center,
        content: Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            Text("Session Expired",
                style: getSemiBoldStyle(color: Colors.black, fontSize: 16.sp)),
            SizedBox(
              height: 10.h,
            ),
            SvgPicture.asset(
              'assets/svg/security-error.svg',
              height: 200.h,
            ),
            SizedBox(
              height: 10.h,
            ),
            Text(
              "Your session has expired! Please login again",
              textAlign: TextAlign.center,
              style: getSemiBoldStyle(
                  color: AppColors.blackPrimaryMinus1,
                  fontSize: FontSize.s14.sp),
            ),
          ],
        ),
        actions: [
          TextButton(
            child: const Text("OK"),
            onPressed: () {
              Navigator.of(context).pop();
              navigatorKey.currentState
                  ?.pushReplacementNamed(Routes.loginRoute);
            },
          ),
        ],
      );
    },
  );
}

问题 当用新的访问令牌替换过期的访问令牌时,就会出现问题。在状态代码 401 上,我实现了代码来清除凭据、显示会话过期对话框以及删除令牌。但是,它应该仅在刷新令牌过期时出现。看来,在用新的 accesstoken 替换后,只有少数拦截器会使用它,这意味着旧的拦截器使用过期的令牌,从而导致未经授权的情况。

预期行为 预期行为如下:

为每个请求添加标头(工作正常)。 检查每个响应中是否有新的访问令牌。 如果有新的访问令牌,请替换旧的。 如果错误状态代码为 401,请清除所有凭据,并显示会话过期对话框,提示重新登录

问题 如何确保替换新访问令牌后每个拦截器都使用新令牌而不是旧令牌? 如何确保在处理 401 错误之后、拒绝之前清除拦截器?

我在 Flutter 应用程序中遇到了 Dio 包的问题。我已经实现了一个拦截器,为每个请求添加标头,并通过简单地注销来处理 401 响应。但是,实施似乎表现出意外,我正在寻求指导来解决该问题。

这是我的 DioClient 类的简化版本:

class DioClient {
  Dio dio = Dio(BaseOptions(
    connectTimeout: const Duration(seconds: 10),
    receiveTimeout: const Duration(seconds: 100),
    receiveDataWhenStatusError: true,
    followRedirects: true,
    headers: {"Content-Type": 'application/json'},
  ));

  TokenManager tokenManager = TokenManager();
  AppPreference appPreference = AppPreference();

  DioClient() {
    dio.interceptors
      ..clear()
      ..add(InterceptorsWrapper(
        onRequest: (options, handler) async {
          final accessToken = await tokenManager.getAccessToken();
          final refreshToken = await tokenManager.getRefreshToken();
          options.headers['Authorization'] = 'Bearer $accessToken';
          options.headers['Cookie'] = '$refreshToken';
          handler.next(options);
        },
        onError: (DioException error, handler) async {
          if (error.response?.statusCode == 401) {
            dio.interceptors.clear();
            final options = error.response!.requestOptions;
            await tokenManager.deleteTokens();
            Hive.box<User>('userBox').clear();
            await appPreference.logOut();
            showSessionExpiredDialog(navigatorKey.currentContext!);
            return handler.reject(DioException(requestOptions: options));
          }
          return handler.next(error);
        },
        onResponse: (response, handler) async {
          if (response.statusCode == 200 && response.data != null) {
            Map<String, dynamic> responseData = response.data;
            if (responseData.containsKey("newAccessToken")) {
              final newAccessToken = responseData["newAccessToken"];
              if (newAccessToken != null) {
                await tokenManager.writeAccessToken(newAccessToken);
                response.requestOptions.headers['Authorization'] =
                    'Bearer $newAccessToken';
                final refreshToken = await tokenManager.getRefreshToken();
                response.requestOptions.headers['Cookie'] = '$refreshToken';
                return handler.resolve(response);
              }
            }
          }
          return handler.next(response);
        },
      ));
  }
}

void showSessionExpiredDialog(BuildContext context) {
  showDialog(
    barrierDismissible: false,
    context: context,
    builder: (BuildContext context) {
      return AlertDialog(
        alignment: Alignment.center,
        content: Column(
          mainAxisSize: MainAxisSize.min,
          children: [
            Text("Session Expired",
                style: getSemiBoldStyle(color: Colors.black, fontSize: 16.sp)),
            SizedBox(
              height: 10.h,
            ),
            SvgPicture.asset(
              'assets/svg/security-error.svg',
              height: 200.h,
            ),
            SizedBox(
              height: 10.h,
            ),
            Text(
              "Your session has expired! Please login again",
              textAlign: TextAlign.center,
              style: getSemiBoldStyle(
                  color: AppColors.blackPrimaryMinus1,
                  fontSize: FontSize.s14.sp),
            ),
          ],
        ),
        actions: [
          TextButton(
            child: const Text("OK"),
            onPressed: () {
              Navigator.of(context).pop();
              navigatorKey.currentState
                  ?.pushReplacementNamed(Routes.loginRoute);
            },
          ),
        ],
      );
    },
  );
}

问题 当用新的访问令牌替换过期的访问令牌时,就会出现问题。在状态代码 401 上,我实现了代码来清除凭据、显示会话过期对话框以及删除令牌。但是,它应该仅在刷新令牌过期时出现。看来,在用新的 accesstoken 替换后,只有少数拦截器会使用它,这意味着旧的拦截器使用过期的令牌,从而导致未经授权的情况。

预期行为 预期行为如下:

为每个请求添加标头(工作正常)。 检查每个响应中是否有新的访问令牌。 如果有新的访问令牌,请替换旧的。 如果错误状态代码为 401,请清除所有凭据,并显示会话过期对话框,提示重新登录

问题 如何确保替换新访问令牌后每个拦截器都使用新令牌而不是旧令牌? 如何确保在处理 401 错误之后、拒绝之前清除拦截器?

flutter authentication http interceptor dio
1个回答
0
投票

您可以尝试以下方法。我没有调试您的代码或任何内容,但下面是拦截器类的示例。如果你像这样实现你的拦截器,它应该可以工作。

class Logging extends Interceptor {
  final Dio dio;

  Logging(this.dio);

  @override
  void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
    log('REQUEST[${options.method}] => PATH: ${options.path} \n Extras: ${options.extra}');
    super.onRequest(options, handler);
  }

  @override
  void onResponse(Response response, ResponseInterceptorHandler handler) {
    log('RESPONSE[${response.statusCode}] => res:: ${json.encode(response.data)}');
    super.onResponse(response, handler);
  }

  @override
  void onError(DioError err, ErrorInterceptorHandler handler) {
    log('ERROR[${err.response!.statusCode ?? 'Unknown Status Code'}] => PATH: ${err.requestOptions.path}');
    super.onError(err, handler);
  }
}

然后只需将拦截器添加到你的 dio 客户端即可。

dio.interceptors.addAll({Logging(dio)});
© www.soinside.com 2019 - 2024. All rights reserved.