Flutter+GetX中Firebase Auth登出触发PERMISSION_DENIED错误求助
问题:Flutter + GetX 登出时触发Firestore权限拒绝错误
调用FirebaseAuth.instance.signOut()执行登出操作时,触发PERMISSION_DENIED权限错误,推测存在未取消的Firestore查询流,但无法定位具体位置。尝试通过isSignOut布尔值控制Firebase服务调用、销毁GetX控制器等操作后,问题仍未解决。
错误信息
Exception has occurred. PlatformException (PlatformException(firebase_firestore, com.google.firebase.firestore.FirebaseFirestoreException: PERMISSION_DENIED: Missing or insufficient permissions., {code: permission-denied, message: The caller does not have permission to execute the specified operation.}, null))
相关代码
HomePage 代码
// ignore: must_be_immutable class HomePage extends StatelessWidget { String phoneNumber; HomePage({super.key, required this.phoneNumber}); bool isSignOut = false; SitesControllers sitesControllers = Get.put(SitesControllers()); int actionController = 0; final fbServices = FirebaseServices(); @override Widget build(BuildContext context) { return GetBuilder<SitesControllers>( init: SitesControllers(), initState: (_) {}, builder: (sitesControllers) { if (isSignOut == false ) { fbServices.saveToken(phoneNumber); sitesControllers.getSites(phoneNumber); } return Scaffold( backgroundColor: Colors.white, appBar: buildAppBarField("Home Page", false), // ignore: unnecessary_null_comparison body: sitesControllers.sitesList == null ? const Center(child: Text("no site"),) : buildSiteField(context, phoneNumber), floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked, floatingActionButton: buildFloatActionButtonField(), bottomNavigationBar: buildBottomNavigationBarField(), ); }, ); } AppBar buildAppBarField(String title, bool back) { return AppBar( automaticallyImplyLeading: back, iconTheme: const IconThemeData(color: ColorConstants.mainColor), elevation: 0.7, centerTitle: true, backgroundColor: Colors.white, title: Text( title, style: GoogleFonts.poppins( fontWeight: FontWeight.bold, color: ColorConstants.mainColor), ), actions: [ PopupMenuButton( itemBuilder: (context) { return [ PopupMenuItem( value: 1, child: Row( children: [const Icon(Icons.unsubscribe), Text(" Unsubscribe", style: GoogleFonts.poppins(),)], )) ]; }, onSelected: (value) { actionController = value; }, ) ], ); } Widget buildSiteField(BuildContext context, String number) { return Padding( padding: EdgeInsets.symmetric(vertical: Get.height*0.02), child: Obx(() => ListView.builder( itemCount: sitesControllers.sitesList.length, itemBuilder: (BuildContext context, index) { if(isSignOut == false){ fbServices.unreadCounter(number, sitesControllers.sitesList[index].id.toString()); } return Padding( padding: EdgeInsets.symmetric( horizontal: Get.width*0.05, vertical: Get.height*0.008), child: Container( decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(15), border: Border.all( color: ColorConstants.mainColor, width: 2)), child: ListTile( visualDensity: VisualDensity.compact, dense: true, isThreeLine: true, onTap: () async { await FirebaseAnalyticsServices.analytics.logEvent( name: "visit_to_site", parameters: null ); if(actionController == 1 ) { } else { buildListTileOnTapField(index, number); } }, leading: buildListTileLeadingField( context, sitesControllers.sitesList[index].name.toString(), index), subtitle: buildListTileSubstitleField(index), trailing: buildListTileTrailingField(context, index, number), )), ); })), ); } Widget buildListTileLeadingField( BuildContext context, String siteName, int index) { return Text( siteName.substring(0, 1).toUpperCase(), style: GoogleFonts.poppins( color: Colors.black, fontSize: 27, fontWeight: FontWeight.w600), ); } Widget buildListTileSubstitleField(int index) { return Text( sitesControllers.sitesList[index].name.toString(), style: GoogleFonts.poppins( fontSize: 15, ) ); } Widget buildListTileTrailingField(BuildContext context, int index, String number) { return actionController == 1 ? Padding( padding: EdgeInsets.symmetric(vertical: Get.height*0.005), child: IconButton( iconSize: 20, icon: const Icon(Icons.unsubscribe), onPressed: () { Get.defaultDialog( textCancel: "CANCEL", textConfirm: "OK", middleText: "are you sure unsubscribe to this site?", onConfirm: () { FirebaseServices().unsubscribeToSite(sitesControllers.sitesList[index].id.toString(), number); Get.back(); }, onCancel: () {}, buttonColor: ColorConstants.mainColor, cancelTextColor: ColorConstants.mainColor, confirmTextColor: Colors.white); }, color: ColorConstants.deleteColor, )) : sitesControllers.sitesList[index].count == 0 ? const Icon(Icons.arrow_circle_right, color: ColorConstants.mainColor,) : Padding( padding: EdgeInsets.symmetric(vertical: Get.height*0.005), child: Container( decoration: const BoxDecoration( shape: BoxShape.circle, color: Colors.red), child: Center( child: Text(sitesControllers.sitesList[index].count.toString(), style: const TextStyle( fontSize: 12, fontWeight: FontWeight.bold, color: Colors.white),), ))); } Widget buildFloatActionButtonField() { return actionController == 1 ? FloatingActionButton( onPressed: () { actionController = 0; }, backgroundColor: ColorConstants.deleteColor, child: const Icon(Icons.arrow_back), ) : FloatingActionButton( onPressed: () { Get.to(() => SubscribeToSite( phoneNumber: phoneNumber, )); }, backgroundColor: ColorConstants.mainColor, child: const Icon(Icons.add), ); } Widget buildBottomNavigationBarField() { return Container( decoration: const BoxDecoration( border: Border( top: BorderSide(color: ColorConstants.borderColor, width: 1.5))), child: BottomAppBar( height: 55, notchMargin: 0.0, elevation: 8, shape: const CircularNotchedRectangle(), child: Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ IconButton( onPressed: () { Get.to(() => ProfilePage(phoneNumber: phoneNumber,)); }, icon: const Icon(Icons.person_3), color: ColorConstants.borderColor), IconButton( onPressed: () { // fix it Get.defaultDialog( textCancel: "NO", textConfirm: "YES", middleText: "are you sure to quit?", onConfirm: () { isSignOut = true; Get.delete<SitesControllers>(); Get.delete<MessagesControllers>(); Get.delete<SubscriberController>(); PhoneAuthController().quitFirebaseAuth(); }, onCancel: () {}, buttonColor: ColorConstants.mainColor, cancelTextColor: ColorConstants.mainColor, confirmTextColor: Colors.white); }, icon: const Icon(Icons.logout), color: ColorConstants.borderColor, ) ], ), ), ); } // FUNCTIONS void buildListTileOnTapField(int index, String number) { String siteKey = sitesControllers.sitesList[index].id.toString(); if(isSignOut == false ){ fbServices.readAllMessages(phoneNumber, siteKey); } Get.to(() => MessagesPage( number: number, siteKey: sitesControllers.sitesList[index].id.toString(), siteName: sitesControllers.sitesList[index].name.toString(), )); } }
SitesControllers 代码
class SitesControllers extends GetxController { List<Sites> sitesList = <Sites>[].obs; Future<void> getSites (String number) async { try { QuerySnapshot sites = await FirebaseFirestore.instance.collection("subscribers").doc(number).collection("sites").get(); sitesList.clear(); for(final site in sites.docs){ sitesList.add( Sites( site.id, site['code'], site['name'], site['status'], site['count'] ) ); } update(); } catch (e) { Get.snackbar( "SitesControllers, ERROR", "> $e", snackPosition: SnackPosition.BOTTOM); } } }
PhoneAuthController 代码
class PhoneAuthController extends GetxController { static PhoneAuthController instance = Get.find(); static late Rx<User?> _user; FirebaseAuth auth = FirebaseAuth.instance; CollectionReference subscribers = FirebaseFirestore.instance.collection("subscribers"); @override void onReady(){ super.onReady(); _user = Rx<User?>(auth.currentUser); _user.bindStream(auth.userChanges()); ever(_user, _initialScreen); } Future<void> quitFirebaseAuth() async { await auth.signOut(); } _initialScreen(User? user){ if(user==null){ Get.offAll(()=>const VerifyByNumber()); } else { String number = user.phoneNumber.toString().substring(1); var subscriber = subscribers.doc(number); subscriber.update({"userId": user.uid}); Get.offAll(()=>HomePage(phoneNumber: number,)); } } }
FirebaseServices 代码
import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; class FirebaseServices extends GetxController { final FirebaseMessaging messaging = FirebaseMessaging.instance; void saveToken(String phoneNumber) async { await messaging.getToken().then((token) { final users = FirebaseFirestore.instance.collection("subscribers").doc(phoneNumber); users.update({"fcmToken": token}); }); } void userEdit(String userPhone, String name, String email, String password) async { try { final users = FirebaseFirestore.instance.collection("subscribers").doc(userPhone); users.update({ "name": name, "email": email, "password": password, }); Get.snackbar("Save Changes", "Succesfully!"); } catch (e) { Get.snackbar("Save Changes", "ERROR: $e"); } } void subscribeToSite(String siteKey, String number) { try { // ignore: unused_local_variable CollectionReference subscriber = FirebaseFirestore.instance .collection("subscribers") .doc(number) .collection("sites"); CollectionReference sites = FirebaseFirestore.instance.collection("sites"); sites.doc(siteKey).update({ "subscribers": FieldValue.arrayUnion([number]) }); } catch (e) { debugPrint("error $e"); } } void unsubscribeToSite(String siteKey, String number) { try { CollectionReference subscribersSite = FirebaseFirestore.instance .collection("subscribers") .doc(number) .collection("sites"); CollectionReference sites = FirebaseFirestore.instance.collection("sites"); sites.doc(siteKey).update({ "subscribers": FieldValue.arrayRemove([number]) }); subscribersSite.doc(siteKey).delete(); } catch (e) { debugPrint("error $e"); } } void topic(String topic) async { await messaging.subscribeToTopic(topic); } void readAllMessages(String phoneNumber, String siteId) async { QuerySnapshot messages = await FirebaseFirestore.instance .collection("subscribers") .doc(phoneNumber) .collection("sites") .doc(siteId) .collection("messages") .get(); // ignore: avoid_function_literals_in_foreach_calls messages.docs.forEach((m) async { final message = FirebaseFirestore.instance .collection("subscribers") .doc(phoneNumber) .collection("sites") .doc(siteId) .collection("messages") .doc(m.id); message.update({"unread": false}); }); } void unreadCounter(String phoneNumber, String siteId) async { FirebaseFirestore.instance .collection("subscribers") .doc(phoneNumber) .collection("sites") .doc(siteId) .collection("messages") .where("unread", isEqualTo: true) .get() .then((QuerySnapshot querySnapshot) { final site = FirebaseFirestore.instance.collection("subscribers").doc(phoneNumber).collection("sites").doc(siteId); site.update({'count': querySnapshot.size}); }); } }
Firestore 规则
rules_version = '2'; service cloud.firestore { match /databases/{database}/documents { match /subscribers/{document=**} { allow read, write: if isSignIn(); match /sites/{site} { allow read, write: if isSignIn(); match /messages/{message}{ allow read, write: if isSignIn(); } } } } function isSignIn(){ return request.auth.uid != null; } }
解决方案
1. 修复GetX控制器重复初始化问题
HomePage中通过Get.put(SitesControllers())已经创建了控制器实例,但GetBuilder里又用init: SitesControllers()重复初始化,导致多个控制器实例存在,登出时Get.delete()无法清理所有实例。修改GetBuilder代码:
return GetBuilder<SitesControllers>( // 移除init参数,使用已存在的实例 builder: (sitesControllers) { // ... 原有逻辑 }, );
2. 优化Firestore操作时机,避免重复查询
当前buildSiteField中每次列表构建都会调用unreadCounter,导致频繁触发Firestore查询,登出后可能仍有未完成的请求。将unreadCounter调用移到getSites方法中,一次性处理所有站点的未读计数:
// 在SitesControllers的getSites方法中修改 Future<void> getSites (String number) async { try { QuerySnapshot sites = await FirebaseFirestore.instance.collection("subscribers").doc(number).collection("sites").get(); sitesList.clear(); for(final site in sites.docs){ sitesList.add( Sites( site.id, site['code'], site['name'], site['status'], site['count'] ) ); // 在这里调用unreadCounter,避免列表构建时重复执行 FirebaseServices().unreadCounter(number, site.id); } update(); } catch (e) { Get.snackbar( "SitesControllers, ERROR", "> $e", snackPosition: SnackPosition.BOTTOM); } }
3. 调整登出流程顺序,先清理Firestore操作
登出时先触发控制器的销毁逻辑,取消所有Firestream监听,再执行登出操作:
// 修改HomePage中登出对话框的onConfirm逻辑 onConfirm: () { // 先触发控制器的销毁逻辑,取消Firestore监听 sitesControllers.onClose(); // 删除所有相关控制器 Get.delete<SitesControllers>(); Get.delete<MessagesControllers>(); Get.delete<Subscriber
相关产品推荐
相关产品推荐

