找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

1163

积分

0

好友

163

主题
发表于 昨天 22:29 | 查看: 1| 回复: 0

引言

在移动应用开发中,认证状态管理是一项基础且关键的任务。一个典型的场景是:当访问令牌过期,API 返回 401 Unauthorized 错误时,应用需要能够自动刷新令牌,并且在此过程中保证用户体验不受影响。

然而,当应用在短时间内并发发起多个网络请求时,问题会变得复杂。若此时令牌恰好过期,所有请求都可能因认证失败而触发刷新逻辑,从而引发竞态条件,导致重复刷新、资源浪费甚至逻辑错误。

本文将详细介绍如何在 Flutter 应用中,利用 Dio 拦截器 与 Dart 的 Completer 类,构建一个优雅的锁定机制,从而高效、可靠地解决多请求场景下的令牌自动刷新问题。

问题:多重刷新引发的混乱

试想一下,如果不加控制地处理并发请求下的令牌过期,会发生什么?

假设应用启动时,需要同时调用多个 API 来获取用户资料、通知、动态等信息。如果此时访问令牌已过期,可能会发生如下情况:

  1. 请求 A 失败(401)-> 触发令牌刷新流程。
  2. 请求 B 失败(401)-> 再次触发令牌刷新流程。
  3. 请求 C 失败(401)-> 又一次触发令牌刷新流程。

这会导致服务器收到多个重复的刷新请求,浪费资源,并可能引发错误(例如,第一个请求刷新出的新令牌被后续的刷新请求立即置为失效)。

Flutter Dio拦截器实现Token自动刷新:解决并发请求的竞态条件 - 图片 - 1

从网络日志中可以看到,多个请求(红色标记)因使用无效令牌访问而连续失败,状态码均为 401。

解决方案:使用 Completer 进行请求排队

解决这一问题的核心是引入一个锁定机制,其逻辑非常清晰:

  1. 发起者(Initiator): 当某个请求检测到令牌即将或已经过期时,它将发起令牌刷新流程。
  2. 队列(Queue): 在刷新进行期间,所有后续到达的请求都必须进入等待状态。
  3. 释放(Release): 令牌刷新完成后,通知所有等待的请求,它们将使用新获取的令牌继续执行。

在 Dart 语言中,Completer 是实现这种“等待-通知”模式的理想工具,它允许我们创建一个 Future 并在将来某个时刻手动完成它。

逻辑流程

本文实现的拦截器选择在 onRequest 阶段执行令牌检查逻辑。这种做法的优势在于,它主动预防因过期令牌导致的网络请求失败,而不是在收到 401 错误后再进行响应,从而使整个请求流程更加顺畅。

令牌拦截器的核心时序逻辑如下图所示:

Flutter Dio拦截器实现Token自动刷新:解决并发请求的竞态条件 - 图片 - 2

根据拦截器的设计,其工作流程可以总结为以下几步:

  1. 读取令牌过期时间:在转发请求前,拦截器首先检查存储的令牌过期时间戳。
  2. 检查锁定状态:通过判断 _completer 变量是否不为 null,来确认当前是否有令牌刷新流程正在进行。
  3. 如果已锁定:若有刷新正在进行,后续到达的请求会通过 await _completer.future 暂停执行,等待刷新完成。
  4. 如果未锁定:若没有进行中的刷新,拦截器将初始化 _completer = Completer() 来“上锁”,接着执行令牌刷新、更新本地存储,最后调用 _completer.complete() 来“解锁”并通知所有等待的请求。

通过在 onRequest 中集中处理令牌的检查与刷新,确保了所有发出的请求都尽可能携带有效的令牌,有效避免了因认证失败引发的网络错误,并将刷新逻辑集中化、同步化。

分步实现

接下来,我们将使用流行的 dio 网络库进行具体的代码实现。在 Flutter 项目中处理移动端的网络请求时,一个健壮的拦截器是提升稳定性的关键。

设置拦截器和“锁”

首先,定义拦截器类并声明 _completer 作为控制访问的“锁”。

class TokenInterceptor extends Interceptor {
  final TokenRepository repository;
  // 这个 completer 作为我们的锁定机制
  Completer<void>? _completer;

  TokenInterceptor({TokenRepository? repository})
      : repository = repository ?? TokenRepository();

  // ...
}
阻塞机制

onRequest 方法中,我们首先检查当前是否已存在一个进行中的刷新流程(即 _completer 是否已被创建)。

@override
Future<void> onRequest(
    RequestOptions options, RequestInterceptorHandler handler) async {
  // ...

  // 条件:如果 completer 不为 null(表示刷新正在进行中)
  // 并且当前请求路径不是令牌刷新端点本身(避免死锁)
  if (_completer != null && options.path != endPointToken) {
    // 在此暂停,直到 future 完成
    await _completer?.future;
    // 完成后,令牌已刷新。更新请求头。
    final headers = options.headers;
    headers[HttpHeaders.authorizationHeader] = repository.accessToken;
    return handler.next(options);
  }

  // ...
}

这段代码确保了,如果“请求 A”正在刷新令牌(创建了 _completer),那么“请求 B”会在 await _completer?.future 处暂停,直到“请求 A”调用 _completer.complete() 后才继续执行。

预防性令牌刷新

我们选择一种更优的策略:在发送请求前主动检查令牌的剩余有效期,而不是被动等待服务器返回 401 错误。

在本实现中,当访问令牌的剩余有效时间小于或等于 10 秒时,就会触发刷新。这样可以确保发出的请求总是携带一个在短期内有效的令牌,从源头上预防了因令牌瞬间过期导致的网络错误,这对于优化HTTP请求成功率很有帮助。

/// ...
  // 计算时间差
  final differenceTime = expiredTime?.difference(currentTime);
  // 如果令牌在 10 秒或更短时间内过期,则进行刷新
  final isTokenExpired = (differenceTime?.inSeconds ?? 0) <= 10;
  final shouldGetToken = repository.shouldGetToken ?? false;
/// ...
执行刷新

如果判断需要刷新令牌,我们将初始化 _completer(上锁),执行刷新请求,然后解锁。

/// ...
if (shouldGetToken || isTokenExpired) {
    try {
      // 1. 上锁:初始化 completer
      _completer = Completer<void>();
      // 2. 执行操作:从 repository 获取新令牌
      final response = await repository.obtainToken();
      if (response == null) {
        throw ArgumentError('response is null');
      }
      // 3. 保存:持久化新令牌
      await saveToken(response);
      final accessToken = response.accessToken;
      // 为当前这次发起的请求更新请求头
      final headers = options.headers;
      headers[HttpHeaders.authorizationHeader] = accessToken;
      final newOption = options.copyWith(headers: headers);
      handler.next(newOption); // 转发发起者请求
      // 4. 解锁:通知所有等待的请求
      if (handler.isCompleted) {
        _completer?.complete();
        _completer = null;
      }
      return;
    } catch (e) {
      // 即使发生错误,也要始终确保解锁
      _completer?.complete();
      _completer = null;
      return handler.next(options);
    }
  }
/// ...

结果:无缝的用户体验

在实现了 TokenInterceptor 并集成到 Dio 实例后,让我们观察应用的实际表现。在下方的网络日志截图中可以看到,尽管同时发起了多个请求,但没有一个因为 401 错误而失败。拦截器无缝地处理了令牌过期问题。系统仅进行了一次令牌刷新(对应日志中耗时较长的那个请求),而所有其他请求都自动排队等待,并在刷新成功后全部成功完成,状态码为 200 OK

Flutter Dio拦截器实现Token自动刷新:解决并发请求的竞态条件 - 图片 - 3

结论

处理令牌过期不仅关乎错误处理,更是一种对网络请求流程的优化。通过结合 Dio Interceptor 与 Dart 的 Completer,我们成功地将可能引发混乱的竞态条件转化为一个有序、高效的请求队列。

关键要点总结:

  1. 逻辑集中化:使用拦截器将认证相关的逻辑与业务 UI 代码分离,使架构更清晰。
  2. 主动检查策略:在请求发出前进行令牌有效期检查(预防性检查),可以减少不必要的网络往返,降低延迟。
  3. 实施锁定机制:利用 Completer 防止并发场景下的重复刷新调用,确保令牌刷新操作的原子性和数据完整性。



上一篇:海康威视技术岗位工作体验深度解析:安防行业研发、支持与职业发展现状
下一篇:智能体互联AHA方案解析:为手机厂商与超级APP构建多端协同新生态
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2025-12-17 17:36 , Processed in 0.158899 second(s), 40 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2025 云栈社区.

快速回复 返回顶部 返回列表