You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Flutter中结合Provider与Dio实现Token刷新的问题求助

Flutter中结合Provider与Dio实现Token刷新的问题求助

各位好,我现在开发Flutter应用时碰到了Token刷新相关的问题,想请大家帮忙排查下。我的项目用Provider做状态管理、Dio处理网络请求,架构是MVVM,用repositories和services作为数据层,路由用go_router,通过它的refreshListenable监听AuthRepository的isAuthenticated getter,当状态变为false时自动跳转到登录页。

先给大家贴出关键代码片段:

AuthRepository实现类

class AuthRepositoryImpl extends AuthRepository {
  AuthRepositoryImpl({
    required AuthApiService authApiService,
    required GoogleAuthService googleAuthService,
    required SecureStorageService secureStorageService,
  }) : _authApiService = authApiService,
       _googleAuthService = googleAuthService,
       _secureStorageService = secureStorageService;

  final AuthApiService _authApiService;
  final GoogleAuthService _googleAuthService;
  final SecureStorageService _secureStorageService;

  bool? _isAuthenticated;

  @override
  Future<bool> get isAuthenticated async {
    // 优先返回缓存的认证状态
    if (_isAuthenticated != null) {
      return _isAuthenticated!;
    }
    // 未缓存则从安全存储中获取
    final result = await _secureStorageService.getAccessToken();
    switch (result) {
      case Ok<String?>():
        _isAuthenticated = result.value != null;
      case Error<String?>():
        _log.severe(
          'Failed to fetch access token from secure storage',
          result.error,
        );
    }
    return _isAuthenticated ?? false;
  }

  // ... 其他未贴出的业务方法
}

MultiProvider依赖配置

List<SingleChildWidget> get providers {
  return [
    Provider(create: (context) => SecureStorageService()),
    Provider(create: (context) => GoogleAuthService()),
    Provider(
      create: (context) => ApiClient(secureStorageService: context.read()),
    ),
    Provider(create: (context) => AuthApiService(apiClient: context.read())),
    ChangeNotifierProvider(
      create: (context) => AuthRepositoryImpl(
        authApiService: context.read(),
        googleAuthService: context.read(),
        secureStorageService: context.read(),
      ) as AuthRepository,
    ),
  ];
}

ApiClient(Dio封装类)

class ApiClient {
  ApiClient({required SecureStorageService secureStorageService})
      : _secureStorageService = secureStorageService,
        _client = _instantiateApiClient(),
        _rawClient = _instantiateApiClient() {
    _client.interceptors.addAll([
      RequestHeadersInterceptor(secureStorageService: _secureStorageService),
      PrettyDioLogger(
        requestHeader: true,
        requestBody: true,
        responseBody: true,
        enabled: kReleaseMode == false,
      ),
      UnauthorizedInterceptor(
        mainClient: _client,
        rawClient: _rawClient,
        secureStorageService: _secureStorageService,
      ),
    ]);
  }

  final Dio _client;
  final Dio _rawClient;
  final SecureStorageService _secureStorageService;

  Dio get client => _client;

  static Dio _instantiateApiClient() {
    return Dio(
      BaseOptions(
        baseUrl: 'https://localhost:5000',
        headers: {'Content-Type': 'application/json'},
      ),
    );
  }
}

处理401的UnauthorizedInterceptor

这个拦截器用来实现Token自动刷新,我用了信号量确保只有第一个401请求触发刷新,其余请求等待刷新完成后重试:

class UnauthorizedInterceptor extends Interceptor {
  UnauthorizedInterceptor({
    required Dio mainClient,
    required Dio rawClient,
    required SecureStorageService secureStorageService,
  }) : _rawClient = rawClient,
       _mainClient = mainClient,
       _secureStorageService = secureStorageService;

  final Dio _mainClient;
  final Dio _rawClient;
  final SecureStorageService _secureStorageService;

  // 控制Token刷新的信号量
  bool _isRefreshing = false;
  // 等待刷新完成的Completer
  Completer<void>? _refreshTokenCompleter;

  @override
  void onError(DioException err, ErrorInterceptorHandler handler) async {
    if (err.response?.statusCode != 401 && !err.requestOptions.path.contains('/auth/refresh')) {
      return handler.next(err);
    }

    if (_isRefreshing) {
      // 已有刷新请求在进行,等待完成
      if (_refreshTokenCompleter != null) {
        try {
          await _refreshTokenCompleter!.future;
        } on Exception {
          _logoutUser();
          return handler.next(err);
        }
      }
      // 重试原请求
      return await _repeatRequestAfterTokenRefresh(err, handler);
    }

    // 触发Token刷新流程
    await _refreshToken(err, handler);
  }

  Future<void> _refreshToken(
    DioException originalError,
    ErrorInterceptorHandler handler,
  ) async {
    _isRefreshing = true;
    _refreshTokenCompleter = Completer<void>();

    try {
      final refreshTokenResult = await _secureStorageService.getRefreshToken();
      if (refreshTokenResult is Error) {
        return _logoutUser();
      }
      final refreshToken = (refreshTokenResult as Ok<String>).value;

      final refreshTokenResponse = await _rawClient.post(
        '/auth/refresh',
        options: Options(headers: {'Authorization': refreshToken}),
      );
      final data = TokenRefreshResponse.fromJson(refreshTokenResponse.data);
      // 此处代码未贴完,后续逻辑应为保存新的accessToken和refreshToken到安全存储
      // ...
    } catch (e) {
      // 刷新失败则执行登出
      _logoutUser();
      handler.next(originalError);
    } finally {
      _isRefreshing = false;
      _refreshTokenCompleter?.complete();
    }
  }

  // 省略_repeatRequestAfterTokenRefresh和_logoutUser方法实现
  // ...
}

现在我遇到的问题:

  1. Token刷新成功后,AuthRepository的_isAuthenticated缓存状态没有更新,导致go_router的refreshListenable无法感知到状态变化,状态管理和实际认证状态不同步
  2. 不确定UnauthorizedInterceptor的逻辑是否有潜在漏洞,比如多个请求等待刷新时的异常处理、刷新失败后的登出逻辑是否严谨

想请教大家:

  • 如何让AuthRepository在Token刷新成功/失败后,正确更新_isAuthenticated缓存,并且通知go_router的refreshListenable状态变化?
  • 我的UnauthorizedInterceptor逻辑有哪些可以优化的地方?
  • 整个Token刷新流程还有哪些需要注意的细节,能让它更健壮?

麻烦各位帮忙分析下,谢谢啦!

内容来源于stack exchange

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.08 14:00:32