Flutter中Firestore结合Firebase Auth实现三字段登录验证问题求助
问题:Firestore与Firebase Auth结合的登录验证逻辑困境
登录页面包含Country、邮箱和密码三个字段,需要通过这三个字段完成用户验证。当前困境是:需要从Firestore获取用户的country字段进行验证,但Firestore获取该字段必须先通过Firebase Auth获取currentUser,无法实现先验证country再完成认证的逻辑。尝试在AuthPage中获取Firestore的country字段,但无法在LoginPage中用于验证,求解决方案。
LoginPage代码
import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:whatsapp/Pages/reset_page.dart'; import 'package:whatsapp/Pages/sign_up.dart'; import 'package:whatsapp/auth/auth_page.dart'; import 'package:whatsapp/components/dropdown_button.dart'; import 'package:whatsapp/components/text_field.dart'; class LoginPage extends StatefulWidget { const LoginPage({super.key}); @override State<LoginPage> createState() => _LoginPageState(); } class _LoginPageState extends State<LoginPage> { final formKey = GlobalKey<FormState>(); String? name; final TextEditingController _emailController = TextEditingController(); final TextEditingController _passwordController = TextEditingController(); bool obscureText = true; String? _selectedCountry; String? countryValue; String? errorMessage; String? errorMessageEmail; String? errorMessagePassword; String? errorMessageCountry; final AuthPage _authPage = AuthPage(); _validateFields() async { String email = _emailController.text; String password = _passwordController.text; if (_selectedCountry == null) { setState(() { errorMessageCountry = 'Please select your country'; }); return; } if (email.isEmpty || !email.contains("@")) { setState(() { errorMessageEmail = 'Please enter a valid E-mail'; }); return; } if (password.isEmpty) { setState(() { errorMessagePassword = 'Please enter your password'; }); return; } errorMessage = null; errorMessageEmail = null; errorMessagePassword = null; errorMessageCountry = null; String? authResult = await _authPage.loginUser( context, mounted, email, password, _selectedCountry!); setState(() { errorMessage = authResult; }); } @override Widget build(BuildContext context) { final deviceWidth = MediaQuery.of(context).size.width; final deviceHeight = MediaQuery.of(context).size.height; return PopScope( canPop: false, child: SafeArea( child: Scaffold( backgroundColor: const Color(0xFF101d25), appBar: AppBar( automaticallyImplyLeading: false, toolbarHeight: deviceHeight < 500 ? deviceHeight * 0.15 : deviceHeight * 0.09, centerTitle: true, backgroundColor: const Color(0xFF101d25), title: const Padding( padding: EdgeInsets.only(top: 10), child: Text('Verify your E-mail', style: TextStyle( color: Color(0xFF0ca886), fontWeight: FontWeight.bold)), ), actions: [ IconButton( icon: const Icon(Icons.more_vert, color: Color(0xFF0ca886)), onPressed: () {}, ), ]), body: SingleChildScrollView( child: Center( child: SizedBox( height: deviceHeight < 500 ? deviceHeight * 1.30 : deviceHeight * 0.80, width: deviceWidth < 500 ? deviceWidth * 0.80 : deviceWidth * 0.60, child: Column( children: [ const Padding( padding: EdgeInsets.only(top: 10), child: Text( 'WhatsApp will verify your registred\n e-mail. Enter your country, e-mail and password:', textAlign: TextAlign.center, style: TextStyle(fontSize: 18, color: Colors.white), ), ), SizedBox(height: deviceHeight * 0.05), Form( key: formKey, child: Column( children: [ CustomDropDownButton( value: _selectedCountry, onChanged: (newValue) { setState(() { _selectedCountry = newValue; errorMessageCountry = null; }); }, ), if (errorMessageCountry != null) Text( errorMessageCountry!, style: const TextStyle(color: Colors.red, fontSize: 14), ), SizedBox(height: deviceHeight * 0.03), CustomTextField( labelText: 'E-mail', keyboardType: TextInputType.emailAddress, controller: _emailController, onChanged: (value) { if (value.isNotEmpty && value.contains("@")) { setState(() { errorMessageEmail = null; }); } }, errorText: errorMessageEmail, ), SizedBox(height: deviceHeight * 0.02), CustomTextField( labelText: 'Password', suffixIcon: GestureDetector( onTap: () { setState(() { obscureText = !obscureText; }); }, child: Padding( padding: EdgeInsets.only( top: deviceHeight < 500 ? deviceHeight * 0.055 : deviceHeight * 0.025), child: FaIcon( obscureText ? FontAwesomeIcons.solidEye : FontAwesomeIcons.solidEyeSlash, color: const Color(0xFF0ca886), size: 22), ), ), keyboardType: TextInputType.visiblePassword, obscureText: obscureText, onChanged: (value) { if (value.isNotEmpty) { setState(() { errorMessagePassword = null; }); } }, controller: _passwordController, errorText: errorMessagePassword, ), ], ), ), SizedBox(height: deviceHeight * 0.02), RichText( text: TextSpan(children: [ TextSpan( text: 'Forgot your password?', style: const TextStyle( color: Colors.blue, fontSize: 15), recognizer: TapGestureRecognizer() ..onTap = () { Navigator.push( context, MaterialPageRoute( builder: (context) => const ResetPasswordPage())); }), ]), ), SizedBox(height: deviceHeight * 0.01), Text( errorMessage ?? '', style: const TextStyle( color: Colors.red, fontSize: 16, ), ), SizedBox(height: deviceHeight * 0.01), SizedBox( height: deviceHeight < 500 ? deviceHeight * 0.15 : deviceHeight * 0.06, width: deviceWidth < 500 ? deviceWidth * 0.68 : deviceWidth * 0.38, child: ElevatedButton( style: ElevatedButton.styleFrom( shape: BeveledRectangleBorder( borderRadius: BorderRadius.circular(2), ), backgroundColor: const Color(0xFF0ca886)), onPressed: () { _validateFields(); }, child: Padding( padding: EdgeInsets.symmetric( vertical: deviceWidth * 0.012), child: const Text( 'Enter', style: TextStyle( color: Color(0xFF121a21), fontSize: 18), ), ), ), ), Padding( padding: EdgeInsets.only(top: deviceHeight * 0.02), child: RichText( text: TextSpan(children: [ const TextSpan( text: "Don't have a account yet? ", style: TextStyle(color: Colors.white, fontSize: 14)), TextSpan( text: 'SignUp.', style: const TextStyle( color: Colors.blue, fontSize: 14, fontWeight: FontWeight.bold), recognizer: TapGestureRecognizer() ..onTap = () { Navigator.push( context, MaterialPageRoute( builder: (context) => const SignupPage())); }), ])), ) ], ), ), ), )), ), ); } }
AuthPage代码
import 'package:cloud_firestore/cloud_firestore.dart'; import 'package:firebase_auth/firebase_auth.dart'; import 'package:flutter/material.dart'; import 'package:whatsapp/Pages/home.dart'; class AuthPage { final _auth = FirebaseAuth.instance; final _firestore = FirebaseFirestore.instance; Future<String?> loginUser( BuildContext context, bool mounted, String email, String password, String selectedCountry) async { try { showCircularIndicator(context); // 第一步:通过邮箱查询Firestore获取用户的country final userQuery = await _firestore.collection("users") .where("email", isEqualTo: email) .limit(1) .get(); if (userQuery.docs.isEmpty) { if (!mounted) return null; Navigator.pop(context); return 'User not found'; } final userDoc = userQuery.docs.first; final storedCountry = userDoc.data()['country']; // 验证输入的country是否匹配 if (storedCountry != selectedCountry) { if (!mounted) return null; Navigator.pop(context); return 'Country does not match our records'; } // 第二步:执行Firebase Auth登录 await _auth.signInWithEmailAndPassword(email: email, password: password); if (!mounted) return null; Navigator.pop(context); _navigateToNextScreen(context); } on FirebaseAuthException catch (e) { if (!mounted) return null; Navigator.pop(context); if (e.code == 'invalid-credential') { return 'E-mail or Password invalid.'; } else if (e.code == 'too-many-requests') { return 'Login failed, exceed requests try again soon.'; } } on FirebaseException catch (e) { if (!mounted) return null; Navigator.pop(context); return 'Firestore error: ${e.message}'; } return null; } void _navigateToNextScreen(BuildContext context) { Navigator.push( context, MaterialPageRoute(builder: (context) => const HomePage()), ); } void showCircularIndicator(BuildContext context) { showDialog( context: context, barrierDismissible: false, builder: (BuildContext context) { return const Center( child: CircularProgressIndicator(color: Color(0xFF0ca886)), ); }); } }
解决方案说明
方案一:先查Firestore验证Country,再执行Auth登录
- 逻辑调整:不再先登录再验证country,而是先通过用户输入的邮箱查询Firestore中的用户记录,取出存储的country与输入值对比,验证通过后再执行Firebase Auth登录。
- 注意事项:
- 需要在Firestore的
users集合上为email字段创建单字段索引,否则查询会报错。 - 确保注册时每个邮箱仅对应一个用户(即邮箱唯一),避免查询返回多条记录。
- 需要在Firestore的
- 代码修改点:
- LoginPage中修改
_validateFields,直接调用loginUser并传入_selectedCountry,移除原signUserIn方法。 - AuthPage的
loginUser方法新增selectedCountry参数,先执行Firestore查询和country验证,再执行登录。
- LoginPage中修改
方案二:登录后立即验证Country,不匹配则登出
如果担心邮箱查询的安全性(比如暴露用户存在性),可以采用先登录再验证的方式:
- 执行Firebase Auth登录。
- 获取currentUser的uid,查询Firestore获取country。
- 对比输入的country与存储值,不匹配则调用
_auth.signOut(),并返回错误提示。 - 该方案的缺点是用户会短暂处于登录状态,但可以快速完成登出,对用户体验影响较小。
内容的提问来源于stack exchange,提问作者Thiago Ferreira
相关产品推荐
相关产品推荐

