使用Getx+Firebase手机号验证时登录/登出触发Firestore权限拒绝错误
问题:手机号验证登录/登出时触发Firestore权限拒绝,重启应用后恢复正常
现象与报错
通过手机号验证登录或登出应用时,触发Firestore权限拒绝错误,重启应用后可正常使用。报错信息如下:
Exception has occurred.
FirebaseException ([cloud_firestore/permission-denied] The caller does not have permission to execute the specified operation.)
相关代码
Dart手机号验证控制器代码
class PhoneAuthController extends GetxController { static PhoneAuthController instance = Get.find(); 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); } _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,)); } } void verifyPhoneNumber(String uPhoneNumber) { String number = uPhoneNumber.substring(1); auth.verifyPhoneNumber( timeout: const Duration(seconds: 120), phoneNumber: uPhoneNumber, verificationCompleted: (PhoneAuthCredential credential) { auth.signInWithCredential(credential).then((value) {}); }, verificationFailed: (FirebaseAuthException e) { }, codeSent: (String id, int? token) { Get.off(() => VerifyToCode( phoneNumber: number, verifyId: id,)); }, codeAutoRetrievalTimeout: (String verificationID) {}); } void verifyCode(String uPhoneNumber, String verifyId, String smsCode) { Map<String, dynamic> subscriberToSave = { "phone": "+$uPhoneNumber", "name": null, "email": null, "password": null, "fcmToken": null, "date": DateTime.now() }; try { FirebaseFirestore.instance.collection("subscribers").doc(uPhoneNumber).set(subscriberToSave); PhoneAuthCredential credential = PhoneAuthProvider.credential(verificationId: verifyId, smsCode: smsCode); auth.signInWithCredential(credential); Get.offAll(VerificationSuccessfulScreen(number: uPhoneNumber)); } catch (e) { Get.snackbar("verifyCode, ERROR:", ">$e"); } } void quitFirebaseAuth() { FirebaseAuth.instance.signOut(); } }
Firestore安全规则
service cloud.firestore { match /databases/{database}/documents { match /subscribers/{subscriber} { allow get, write: if isSignIn() && request.auth.uid == resource.data.userId && request.auth.token.phone_number == resource.data.phone; match /sites/{site} { allow read, write: if isSignIn(); match /messages/{message}{ allow read, write: if isSignIn(); } } } match /sites/{site} { allow read, write: if isSignIn(); } } function isSignIn(){ return request.auth.uid != null; } }
问题原因分析
- 登录时序错误:
verifyCode方法中先执行Firestore的set操作,此时用户还未完成登录(auth.signInWithCredential是异步操作,未等待完成就执行写入),导致request.auth为空,触发isSignIn()校验失败。 - 权限规则逻辑冲突:
subscribers文档的写入规则要求request.auth.uid == resource.data.userId,但首次创建文档时resource.data不存在,该条件永远不满足,导致首次写入失败。 - 登出状态同步延迟:登出后Getx的
_user流未及时同步Auth状态,后续操作可能基于旧状态触发权限检查失败。
解决办法
1. 修正登录时序,等待Auth登录完成后操作Firestore
修改verifyCode方法,确保登录完成后再写入Firestore,且首次创建文档时就传入userId:
void verifyCode(String uPhoneNumber, String verifyId, String smsCode) { try { PhoneAuthCredential credential = PhoneAuthProvider.credential(verificationId: verifyId, smsCode: smsCode); // 等待登录操作完成 auth.signInWithCredential(credential).then((userCredential) { User? user = userCredential.user; if (user != null) { Map<String, dynamic> subscriberToSave = { "phone": "+$uPhoneNumber", "name": null, "email": null, "password": null, "fcmToken": null, "date": DateTime.now(), "userId": user.uid // 首次创建即写入userId }; FirebaseFirestore.instance.collection("subscribers").doc(uPhoneNumber).set(subscriberToSave); Get.offAll(VerificationSuccessfulScreen(number: uPhoneNumber)); } }); } catch (e) { Get.snackbar("verifyCode, ERROR:", ">$e"); } }
2. 修正Firestore规则,兼容首次创建文档场景
区分create和update操作,调整权限校验逻辑:
service cloud.firestore { match /databases/{database}/documents { match /subscribers/{subscriber} { // 首次创建文档:仅校验登录状态和手机号匹配 allow create: if isSignIn() && request.resource.data.phone == request.auth.token.phone_number; // 更新/读取文档:校验登录状态、userId和手机号匹配 allow get, update: if isSignIn() && request.auth.uid == resource.data.userId && request.auth.token.phone_number == resource.data.phone; match /sites/{site} { allow read, write: if isSignIn(); match /messages/{message}{ allow read, write: if isSignIn(); } } } match /sites/{site} { allow read, write: if isSignIn(); } } function isSignIn(){ return request.auth.uid != null; } }
3. 优化登出后的状态同步
确保登出后立即跳转登录页,避免基于旧状态操作:
void quitFirebaseAuth() { FirebaseAuth.instance.signOut().then((_) { Get.offAll(() => const VerifyByNumber()); }); }
4. 优化_initialScreen中的更新操作
先检查文档是否存在,再执行更新:
_initialScreen(User? user){ if(user==null){ Get.offAll(()=> const VerifyByNumber()); } else { String number = user.phoneNumber.toString().substring(1); var subscriber = subscribers.doc(number); subscriber.get().then((docSnapshot) { if (docSnapshot.exists) { subscriber.update({"userId": user.uid}); } Get.offAll(()=>HomePage(phoneNumber: number,)); }); } }
内容的提问来源于stack exchange,提问作者user14828291
相关产品推荐
相关产品推荐

