如何使用 Riverpod 处理身份验证令牌和 GraphQL 客户端?

How to handle the authentication token and and GraphQL client with Riverpod?

我们正在使用 GraphQL 将我们的后端与我们的移动应用程序连接起来。我们正在使用 Riverpod 来处理全局状态、依赖注入等。

我们正在使用提供程序来处理 GraphQL 客户端,而无需任何身份验证 header 首先用户未进行身份验证(这将处理未经身份验证的请求,例如登录或注册),在用户通过身份验证后,提供令牌的令牌提供者将更新,并且它必须更新作为应用程序上每个存储库的用户的客户端提供者:

final StateProvider<String> tokenProvider =
    StateProvider<String>((_) => '', name: "Token Provider");
final graphQLClientProvider = Provider<GraphQLClient>(
  (ref) {
    final String token = ref.watch(tokenProvider).state;

    final Link _link = HttpLink(
        'http://192.168.1.36:1337/graphql',
        defaultHeaders: {
          if (token != '') 'Authorization': 'Bearer $token',
        });

    return GraphQLClient(
      cache: GraphQLCache(),
      link: _link,
    );
  },
  name: "GraphQL Client Provider",
);

这里问题太多:

  1. 更新令牌后,客户端发生变化,由此可见,更新令牌的函数没有正常完成:
[ +141 ms] E/flutter ( 8361): [ERROR:flutter/lib/ui/ui_dart_state.cc(199)] Unhandled Exception: Bad state: Tried to use AuthNotifier after `dispose` was called.
[        ] E/flutter ( 8361): 
[        ] E/flutter ( 8361): Consider checking `mounted`.
[        ] E/flutter ( 8361): 
[        ] E/flutter ( 8361): #0      StateNotifier._debugIsMounted.<anonymous closure> (package:state_notifier/state_notifier.dart:128:9)
[        ] E/flutter ( 8361): #1      StateNotifier._debugIsMounted (package:state_notifier/state_notifier.dart:135:6)
[        ] E/flutter ( 8361): #2      StateNotifier.state= (package:state_notifier/state_notifier.dart:155:12)
[        ] E/flutter ( 8361): #3      AuthNotifier.signIn (package:thesis_cancer/features/auth/application/auth.notifier.dart:84:7)
[        ] E/flutter ( 8361): <asynchronous suspension>
[        ] E/flutter ( 8361): #4      _LoginCardState._submit (package:flutter_login/src/widgets/auth_card.dart:503:15)
[        ] E/flutter ( 8361): <asynchronous suspension>
[        ] E/flutter ( 8361): 

这意味着当令牌更新时,接收存储库的身份验证通知者取决于提供的客户端是updated/changes(在链之后):

final authRepositoryProvider = Provider<AuthRepository>(
  (ref) => GraphQLAuthRepository(client: ref.read(graphQLClientProvider)),
  name: 'Auth Repository Provider',
);

final authNotifierProvider = StateNotifierProvider<AuthNotifier, AuthState>(
  (ref) => AuthNotifier(
    authRepository: ref.watch(authRepositoryProvider),
    dataStore: ref.watch(dataStoreRepositoryProvider),
    profileRepository: ref.watch(profileRepositoryProvider),
    tokenController: ref.watch(tokenProvider.notifier),
    userController: ref.watch(userEntityProvider.notifier),
  ),
  name: "Authentication Notifier Provider",
);

条件中断的通知函数(if):

Future<String?> signIn({
    required String username,
    required String password,
  }) async {
    try {
      final Map<String, dynamic> rawUser = await authRepository.signIn(
          identifier: username, password: password) as Map<String, dynamic>;
      final User sessionUser = User.fromJson(rawUser);
      if (sessionUser.confirmed != false) {
        tokenController.state = sessionUser.token!; <-- Bug begins here
      }
      await dataStore.writeUserProfile(sessionUser);
      userController.state = sessionUser;
      state = const AuthState.loggedIn();
    } on LogInFailureByBadRequest {
      return "E-posta veya şifre geçersiz.";
    } on LogInFailure catch (error) {
      return error.toString();
    }
  }
  1. 虽然在更新令牌时客户端似乎发生了变化,但客户端本身并不反映更改(它不包括使用令牌的身份验证 header),因此破坏了 GraphQL 客户端:

按照 the issue we opened, we added the ProviderReference to our notifier (following this question) 的评论,但它不起作用:

 AuthNotifier(
    this._ref, {
    required this.authRepository,
    required this.profileRepository,
    required this.dataStore,
    required this.tokenController,
    required this.userController,
  }) : super(const AuthState.loading());
  final ProviderReference _ref;
  final ProfileRepository profileRepository;
  final AuthRepository authRepository;
  final DataStoreRepository dataStore;
  final StateController<User?> userController;
  final StateController<String> tokenController;

至此,我们不明白这里的问题是什么。

更新

我们在 StateNotifier 及其提供程序中删除了此更改。

通过读取获取存储库中的客户端并不能避免错误:

使用 Riverpod 处理授权令牌和 GraphQL 客户端的正确方法是什么?

谢谢。

我认为错误出在您的 AuthNotifier 中:

  if (sessionUser.confirmed != false) {
    tokenController.state = sessionUser.token!; <-- you are updating a dependencie of your actual AuthNotifier object
  }
  await dataStore.writeUserProfile(sessionUser); <-- during the await a new object is created and the actual instance is disposed so this line will throw

问题是 authRepositoryProvider:

final authRepositoryProvider = Provider<AuthRepository>(
  (ref) => GraphQLAuthRepository(client: ref.read(graphQLClientProvider)),
  name: 'Auth Repository Provider',
);

在此片段中,authRepositoryProvider 使用的是 GraphQLClient 没有令牌。更新令牌时,graphQLClientProvider 状态更新良好,但 authRepositoryProvider 不是因为您正在使用 ref.read 避免重建 authRepositoryProvider 状态。这意味着 authRepositoryProvider 知道 graphQLClientProvider 的处置(和卸载)状态,这就是错误消息的原因。

有2种解决方案:

  1. 使用ref.watch
final authRepositoryProvider = Provider<AuthRepository>(
  (ref) => GraphQLAuthRepository(client: ref.watch(graphQLClientProvider)),
  name: 'Auth Repository Provider',
);
  1. ref.read 作为参数传递:
final authRepositoryProvider = Provider<AuthRepository>(
  (ref) => GraphQLAuthRepository(client: ref.read),
  name: 'Auth Repository Provider',
);

class AuthRepository {
  AuthRepository(this.read)

  final Reader read;

  GraphQLClient get client => read(graphQLClientProvider);
}

注意使用 readwatch,第一个不响应更改但可以在任何地方使用,而第二个响应提供者的状态更新但只能在另一个提供者构造函数中使用,或者在提供对 watchref.watch 的访问的小部件中使用,具体取决于 Riverpod 的版本。

另外,你的代码还有一个问题:

  final User sessionUser = User.fromJson(rawUser);
  if (sessionUser.confirmed != false) {
    // In this moment, the token is updated,
    // then the QrahpQLClient is updated,
    // then the AuthNotifier is recreated,
    // i.e the current is disposed
    // and now becomes the previous
    tokenController.state = sessionUser.token!; <-- Bug begins here
  }
  await dataStore.writeUserProfile(sessionUser);
  userController.state = sessionUser;

  // In this moment you are updating the state of a disposed notifier
  state = const AuthState.loggedIn();

要提供解决方案,必须定义哪些元素将重建 AuthNotifer.state 而不是重新创建通知程序。

您必须考虑基于 ref.read 访问另一个通知程序并在 StateController 构造函数中手动创建订阅。

以后riverpod 1.0发布的时候,这个问题用ref.listen就很容易了,但是现在你需要重写你的代码。您可以在 bloc-to-bloc 交流指南中获得灵感:https://bloclibrary.dev/#/architecture?id=bloc-to-bloc-communication