You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

Dio实现Token刷新时偶发401「无效刷新令牌」错误排查求助

Dio实现Token刷新时偶发401「无效刷新令牌」错误排查求助

大家好,我在Flutter项目中基于Dio的QueuedInterceptor实现了Token自动刷新的逻辑——为了防止主Dio实例的拦截器给刷新Token的接口带上过期的accessToken,特意单独创建了_tokenDio实例处理刷新请求。

大部分情况下逻辑都能正常工作:Token过期时自动刷新,并发请求会被队列化,刷新成功后重试所有pending请求。但偶尔会出现401「Invalid Refresh Token」错误,我打印了当时的refreshToken看起来是有效的,但就是刷新失败,实在找不到问题出在哪,想请大家帮忙看看代码有没有疏漏。

我的拦截器代码如下:

class AuthInterceptor extends QueuedInterceptor {
  final Dio _dio;
  final FlutterSecureStorage _secureStorage;
  bool _isRefreshing = false;
  final List<RequestOptions> _pendingRequests = [];

  final Dio _tokenDio = Dio(
    BaseOptions(
      baseUrl: baseURL,
      headers: {
        'accept': 'application/json',
        'Content-Type': 'application/json',
      },
    ),
  )..interceptors.add(
      LogInterceptor(
        request: true,
        requestHeader: true,
        requestBody: true,
        responseBody: true,
      ),
    );

  AuthInterceptor(this._dio, this._secureStorage);

  @override
  void onRequest(RequestOptions options, RequestInterceptorHandler handler) async {
    final accessToken = await _secureStorage.read(key: 'accessToken');
    final refreshToken = await _secureStorage.read(key: 'refreshToken');

    // 确保用的是accessToken而非refreshToken
    if (accessToken != null) {
      options.headers['Authorization'] = 'Bearer $accessToken';
      print('✅ Authorization header set with accessToken');
    } else {
      print('❌ Cannot set Authorization header - accessToken is null');
    }

    super.onRequest(options, handler);
  }

  @override
  void onError(DioException err, ErrorInterceptorHandler handler) async {
    if (err.response?.statusCode == 401) {
      // Token过期,尝试刷新
      if (_isRefreshing) {
        // 正在刷新时,将请求加入队列
        _pendingRequests.add(err.requestOptions);
        return;
      }

      final refreshToken = await _secureStorage.read(key: 'refreshToken');
      if (refreshToken != null) {
        print("The current refresh token is : $refreshToken");
        _isRefreshing = true;

        try {
          final newTokens = await _refreshAccessToken(refreshToken);
          if (newTokens != null) {
            await _secureStorage.write(
              key: 'accessToken',
              value: newTokens['accessToken'],
            );
            await _secureStorage.write(
              key: 'refreshToken',
              value: newTokens['refreshToken'],
            );

            // 重试原始请求
            final requestOptions = err.requestOptions;
            requestOptions.headers['Authorization'] = 'Bearer ${newTokens['accessToken']}';
            final response = await _dio.fetch(requestOptions);
            handler.resolve(response);

            // 处理所有队列中的请求
            await _processPendingRequests(newTokens['accessToken']!);
            return;
          }
        } on DioException catch (e) {
          print('🔴 Token refresh failed (DioException): $e');
          await _logOut();
          handler.reject(err);
          return;
        } catch (e) {
          print('🔴 Token refresh failed: $e');
          await _logOut();
        } finally {
          _isRefreshing = false;
        }
      }

      await _logOut(); // 无有效refreshToken或刷新失败
      handler.reject(err);
    } else {
      super.onError(err, handler);
    }
  }

  Future<Map<String, String>?> _refreshAccessToken(String refreshToken) async {
    final response = await _tokenDio
        .get(
          RefreshToken,
          options: Options(headers: {'Authorization': 'Bearer $refreshToken'}),
        )
        .timeout(Duration(seconds: 20));

    final data = response.data as Map<String, dynamic>;
    print('✅ Token refreshed successfully');
    print('🔑 New accessToken: ${data['accessToken']}');
    print('♻️ New refreshToken: ${data['refreshToken']}');

    return {
      'accessToken': data['accessToken'] as String,
      'refreshToken': data['refreshToken'] as String,
    };
  }

  Future<void> _processPendingRequests(String newAccessToken) async {
    print('🔄 Processing ${_pendingRequests.length} pending requests...');
    for (final requestOptions in _pendingRequests) {
      requestOptions.headers['Authorization'] = 'Bearer $newAccessToken';
      await _dio.fetch(requestOptions);
    }
    _pendingRequests.clear();
  }

  Future<void> _logOut() async {
    await AuthService.logout();
    _pendingRequests.clear();
    _isRefreshing = false;
  }
}

已完成的排查:

  • 确认刷新Token请求用了独立的_tokenDio实例,不会被主拦截器处理,不会携带过期accessToken
  • 触发刷新时会打印当前refreshToken,看起来是有效的,但偶尔仍返回「Invalid Refresh Token」
  • _isRefreshing标记和_pendingRequests队列处理并发,避免重复触发刷新
  • 刷新成功后会更新本地Token并重试所有pending请求

我怀疑的几个方向(但没找到证据):

  • 调用_refreshAccessToken过程中,本地refreshToken会不会被其他操作(比如用户手动退出)修改?
  • 并发场景下,_isRefreshing的锁有没有可能失效,导致多个请求同时调用刷新接口?
  • 新refreshToken有没有可能没正确写入FlutterSecureStorage
  • refreshToken本身已过期,但代码没处理这种情况,直接调用刷新接口导致401?

有没有大佬能帮我看看代码里的逻辑漏洞,或者推荐一些排查方向?谢谢大家!😥

火山引擎 最新活动