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

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登录

  1. 逻辑调整:不再先登录再验证country,而是先通过用户输入的邮箱查询Firestore中的用户记录,取出存储的country与输入值对比,验证通过后再执行Firebase Auth登录。
  2. 注意事项:
    • 需要在Firestore的users集合上为email字段创建单字段索引,否则查询会报错。
    • 确保注册时每个邮箱仅对应一个用户(即邮箱唯一),避免查询返回多条记录。
  3. 代码修改点:
    • LoginPage中修改_validateFields,直接调用loginUser并传入_selectedCountry,移除原signUserIn方法。
    • AuthPage的loginUser方法新增selectedCountry参数,先执行Firestore查询和country验证,再执行登录。

方案二:登录后立即验证Country,不匹配则登出

如果担心邮箱查询的安全性(比如暴露用户存在性),可以采用先登录再验证的方式:

  1. 执行Firebase Auth登录。
  2. 获取currentUser的uid,查询Firestore获取country。
  3. 对比输入的country与存储值,不匹配则调用_auth.signOut(),并返回错误提示。
  4. 该方案的缺点是用户会短暂处于登录状态,但可以快速完成登出,对用户体验影响较小。

内容的提问来源于stack exchange,提问作者Thiago Ferreira

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.29 15:47:32