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
相关产品推荐
相关产品推荐

