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

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
相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.22 12:04:03