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

Flutter Firestore流查询登出切换匿名登录时权限错误处理问题

问题描述

我开发的Flutter应用采用Firestore与Redux状态管理,包含一个Firestore流查询。用户登录时该查询运行正常,但在用户登出并切换至匿名登录的间隙,会触发如下权限错误:

flutter: ----------------FIREBASE CRASHLYTICS----------------
flutter: [cloud_firestore/permission-denied] The caller does not have permission to execute the specified operation.
flutter:
#0      EventChannelExtension.receiveGuardedBroadcastStream (package:_flutterfire_internals/src/exception.dart:67:43)
#1      MethodChannelQuery.snapshots.<anonymous closure> (package:cloud_firestore_platform_interface/src/method_channel/method_channel_query.dart:183:18)

根据官方文档,我了解这是已知场景,需进行错误处理,但即使添加了错误处理块并取消订阅,错误仍持续抛出。

相关代码

GroupService类

class GroupService {
  final FirebaseAuth _auth = FirebaseAuth.instance;
  final FirebaseFirestore _db = FirebaseFirestore.instance;

  StreamController<void> _logoutController = StreamController<void>();

  StreamSubscription<List<Member?>>? _groupMembershipsSubscription;

  GroupService();

  get _getUid {
    firebaseuser.User user = _auth.currentUser!;
    return user.uid;
  }

  // Return user's group memberships from firestore
  Stream<List<Member?>> getGroupMemberships() {
    String uid = _getUid;
      print('GroupService :: getGroupMemberships: Uid: $uid');

    final docRef =
        _db.collection('memberships').where('memberId', isEqualTo: uid).where(
      'status',
    );
    return docRef.snapshots().map((snapshot) {
      List<Member?> memberships = [];
      if (snapshot.docs.isNotEmpty) {
        memberships = snapshot.docs.map((doc) {
          Member? membership =
              serializers.deserializeWith(Member.serializer, doc.data());
          return membership;
        }).toList();
      }
      return memberships;
    }).handleError(
      (error) {
        print('GroupService :: getGroupMemberships : Error: $error');
      },
    );
  }

  // Start listening to the stream and store the subscription
  void startListeningToGroupMemberships({
    required void Function(List<Member?>) onData,
    required void Function(dynamic error) onError,
  }) {
    // Initialize a new logout controller
    _logoutController = StreamController<void>();

    // Create a new subscription
    _groupMembershipsSubscription =
        getGroupMemberships().takeUntil(_logoutController.stream).listen(
      (memberships) {
        onData(memberships);
      },
      onError: (error) {
        onError(error);
      },
    );
  }

  // Cancel the subscription
  void cancelGroupMembershipsSubscription() {
    _logoutController.add(null); // Trigger takeUntil
    _logoutController.close(); // Close the logout stream
    _groupMembershipsSubscription?.cancel();
    _groupMembershipsSubscription = null;
    print('GroupService :: Subscription cancelled');
  }
}

Redux中间件调用代码

void Function(
  Store<AppState> store,
  dynamic action,
  NextDispatcher next,
) _loadGroups() {
  GroupService groupService = GetIt.I.get<GroupService>();
  return (store, action, next) async {
    next(action);

    if (action is LoadGroupsAction) {

      print('MW :: _loadGroups : Called on LoadGroupsAction');

      groupService.startListeningToGroupMemberships(onData: (memberships) {
        store.dispatch(RefreshGroupsAction());
        if (memberships.isEmpty) {
          store.dispatch(LoadGroupSucceededAction(null));
        } else {
          for (Member? m in memberships) {
            try {
              groupService.getGroup(m!.groupId!).then((group) {
                  if (m.status == MemberStatusType.active) {
                    store.dispatch(LoadGroupSucceededAction(group));
                  } else {
                    store.dispatch(LoadNonMemberGroupSucceededAction(group));
                  }
                } else {
                  store.dispatch(LoadGroupSucceededAction(null));
                }
              });
            } catch (e) {
              print('MW :: _loadGroups : Error: $e');
            }
          }
        }
      }, onError: (error) async {
        print('MW :: _loadGroups : Error: $e');
      });

    } else if (action is LogoutAction) {

      print('MW :: _loadGroups : Called on LogoutAction');
      groupService.cancelGroupMembershipsSubscription();

    }
  };
}

请问该如何正确处理这个权限错误?


解决方案

核心问题是登出时订阅取消时机滞后、未处理用户身份为空的场景、错误处理存在漏洞,以下是具体修复步骤:

1. 修复用户UID获取的空安全问题

当前_getUid强制解包currentUser!,登出后currentUser为null会直接崩溃,同时导致Firestore查询使用无效身份触发权限错误。修改为:

String? get _getUid {
  return _auth.currentUser?.uid;
}

2. 避免发起无效查询

在getGroupMemberships中先判断UID是否存在,不存在时直接返回空流,不发起Firestore请求:

Stream<List<Member?>> getGroupMemberships() {
  final uid = _getUid;
  if (uid == null) {
    print('GroupService :: No active user, returning empty stream');
    return Stream.value([]);
  }
  print('GroupService :: getGroupMemberships: Uid: $uid');

  final docRef = _db.collection('memberships')
      .where('memberId', isEqualTo: uid)
      .where('status', isNotEqualTo: null); // 补全原代码中缺失的status查询条件
  return docRef.snapshots().map((snapshot) {
    List<Member?> memberships = [];
    if (snapshot.docs.isNotEmpty) {
      memberships = snapshot.docs.map((doc) {
        Member? membership = serializers.deserializeWith(Member.serializer, doc.data());
        return membership;
      }).toList();
    }
    return memberships;
  }).handleError(
    (error) {
      print('GroupService :: getGroupMemberships : Error: $error');
      // 可在此添加错误上报或其他处理逻辑
    },
  );
}

3. 调整订阅取消的顺序

原取消逻辑先触发takeUntil再取消订阅,可能导致流仍在处理请求。改为先取消订阅再关闭控制器:

void cancelGroupMembershipsSubscription() {
  _groupMembershipsSubscription?.cancel();
  _groupMembershipsSubscription = null;
  _logoutController.add(null);
  _logoutController.close();
  print('GroupService :: Subscription cancelled');
}

4. 监听身份变化自动取消订阅

仅依赖LogoutAction可能存在时机问题,直接监听FirebaseAuth的身份变化,自动处理订阅:

class GroupService {
  // ...现有成员变量
  StreamSubscription? _authStateSubscription;

  GroupService() {
    // 监听用户身份变化
    _authStateSubscription = _auth.authStateChanges().listen((user) {
      if (user == null) {
        // 用户登出,立即取消所有Firestore订阅
        cancelGroupMembershipsSubscription();
      }
    });
  }

  // 添加dispose方法,在服务销毁时清理资源
  void dispose() {
    _authStateSubscription?.cancel();
    cancelGroupMembershipsSubscription();
  }
}

记得在应用退出或服务不再使用时调用dispose()。

5. 修复错误处理中的变量名错误

中间件的onError回调中,打印变量与参数不一致,导致错误信息无法正确输出,修改为:

onError: (error) async {
  print('MW :: _loadGroups : Error: $error');
}

6. 确保登出动作执行前完成订阅取消

在Redux中间件中,处理LogoutAction时先取消订阅,再执行后续登出逻辑:

} else if (action is LogoutAction) {
  print('MW :: _loadGroups : Called on LogoutAction');
  // 先取消订阅
  groupService.cancelGroupMembershipsSubscription();
  // 再执行登出相关操作
  next(action);
}

注:原代码中next(action)在最上方,会先执行登出动作再取消订阅,导致身份失效但流仍在运行,调整顺序即可。


内容的提问来源于stack exchange,提问作者Elvis Ninan

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.14 21:54:59