使用电子邮件链接进行 Firebase 身份验证

借助 Firebase 身份验证,您可以通过以下方式让用户登录:向用户发送包含链接的电子邮件,用户点击链接进行登录。在此过程中,用户的电子邮件地址也会得到验证。

通过电子邮件登录的好处有很多:

  • 提供顺畅的注册和登录体验。
  • 降低在不同应用中重复使用密码的风险。即使是精心挑选的密码,如果重复使用,安全性也会受到影响。
  • 能够在验证用户身份的同时,验证用户的确是电子邮件地址的合法所有者。
  • 用户只需一个可访问的电子邮件帐号即可登录,不需要拥有电话号码或社交媒体帐号。
  • 用户可以安全地登录,而无需提供(或记住)密码。在移动设备上,输密码可能很不方便。
  • 此前使用电子邮件标识符(密码或联合登录服务)登录的现有用户可以升级为仅使用电子邮件登录。例如,用户即使忘记密码也仍可登录,无需重置密码。

准备工作

  1. 按照使用入门指南中的步骤操作(如果您尚未这样做)。

  2. 为 Firebase 项目启用电子邮件链接登录。

    为了让用户能够通过电子邮件链接登录,您必须先为 Firebase 项目启用电子邮件提供方和电子邮件链接登录方法,操作步骤如下:

    1. Firebase 控制台中,打开 Auth 部分。
    2. 登录方法标签页中,启用电子邮件地址/密码提供方。请注意,必须启用电子邮件地址/密码登录才能使用电子邮件链接登录。
    3. 在同一部分中,启用电子邮件链接(无密码登录)登录方法。
    4. 点击保存

如需启动身份验证流程,请显示一个要求用户提供电子邮件地址的界面,然后调用 sendSignInLinkToEmail() 以请求 Firebase 向该用户的电子邮件地址发送身份验证链接。

  1. 构建 ActionCodeSettings 对象,以向 Firebase 提供有关如何构建电子邮件链接的说明。设置以下字段:

    • url:要嵌入的深层链接以及要传递的其他任何状态。您必须在 Firebase 控制台的“已获授权的网域”列表中将链接的网域列入白名单。要找到这些网域,您可以进入“登录方法”标签页(“Authentication”->“登录方法”)。如果用户的设备上尚未安装应用且过去无法安装应用,链接会将用户重定向到此网址。

    • androidPackageNameIOSBundleId:在 Android 或 iOS 设备上打开登录链接时使用的应用。详细了解如何配置 Firebase Dynamic Links,以通过移动应用打开电子邮件操作链接。

    • handleCodeInApp:设置为 true。与其他带外电子邮件操作(密码重置和电子邮件验证)不同,登录操作必须始终在应用中完成。这是因为按照预期,在流程结束时用户应该已经登录,并且他们的身份验证状态应在应用内持久保存。

    • dynamicLinkDomain:如果为项目定义了多个自定义动态链接网域,请选择在通过指定的移动应用打开链接时应使用的网域(例如 example.page.link)。否则,系统会自动选择第一个网域。

    var acs = ActionCodeSettings(
        // URL you want to redirect back to. The domain (www.example.com) for this
        // URL must be whitelisted in the Firebase Console.
        url: 'https://www.example.com/finishSignUp?cartId=1234',
        // This must be true
        handleCodeInApp: true,
        iOSBundleId: 'com.example.ios',
        androidPackageName: 'com.example.android',
        // installIfNotAvailable
        androidInstallApp: true,
        // minimumVersion
        androidMinimumVersion: '12');
    
  2. 要求用户输入电子邮件地址。

  3. 向用户的电子邮件地址发送身份验证链接,并保存用户的电子邮件地址,因为用户可能在同一设备上完成电子邮件登录。

    var emailAuth = 'someemail@domain.com';
    FirebaseAuth.instance.sendSignInLinkToEmail(
            email: emailAuth, actionCodeSettings: acs)
        .catchError((onError) => print('Error sending email verification $onError'))
        .then((value) => print('Successfully sent email verification'));
    });
    

安全考量

为防止有人以非预期用户身份或在非预期设备上使用登录链接登录,Firebase Authentication 要求在完成登录流程时提供用户的电子邮件地址。要成功登录,此电子邮件地址必须与登录链接最初发送到的地址一致。

对于在发出链接请求的同一设备上打开登录链接的用户,您可以在发送登录电子邮件时将他们的电子邮件地址存储在本地(例如使用 SharedPreferences),以简化登录流程。然后,使用这个地址来完成登录。 请勿在重定向网址参数中传递用户的电子邮件地址并重复使用该地址,这可能会带来会话注入风险。

登录完成后,用户之前未经过验证的登录机制都将被移除,现有会话将失效。例如,如果有人在以前使用相同的电子邮件地址和密码创建了一个未经验证的帐号,系统会移除用户的密码,以防止认领了所有权并创建了该未验证帐号的冒名者再次使用未经验证的电子邮件地址和密码登录。

同时,请确保在生产环境中使用 HTTPS 网址,以免您的链接被中间服务器拦截。

Firebase Authentication 使用 Firebase Dynamic Links 将电子邮件链接发送到移动设备。为了通过移动应用完成登录,必须对应用进行配置,使其检测传入的应用链接,解析底层的深层链接,然后完成登录。

  1. 指南中将您的应用设置为在 Flutter 上接收 Dynamic Links。

  2. 在链接处理程序中,检查链接是否用于电子邮件链接身份验证;如果是,请完成登录过程。

    // Confirm the link is a sign-in with email link.
    if (FirebaseAuth.instance.isSignInWithEmailLink(emailLink)) {
      try {
        // The client SDK will parse the code from the link for you.
        final userCredential = await FirebaseAuth.instance
            .signInWithEmailLink(email: emailAuth, emailLink: emailLink);
    
        // You can access the new user via userCredential.user.
        final emailAddress = userCredential.user?.email;
    
        print('Successfully signed in with email link!');
      } catch (error) {
        print('Error signing in with email link.');
      }
    }
    

您还可以将此身份验证方法与现有用户相关联。例如,以前使用其他提供方(例如电话号码)进行身份验证的用户,可以将此登录方法添加到其现有帐号。

不同之处在于操作的后半部分:

final authCredential = EmailAuthProvider
    .credentialWithLink(email: emailAuth, emailLink: emailLink.toString());
try {
    await FirebaseAuth.instance.currentUser
        ?.linkWithCredential(authCredential);
} catch (error) {
    print("Error linking emailLink credential.");
}

这也可以用于在执行敏感操作之前重新验证电子邮件链接用户的身份。

final authCredential = EmailAuthProvider
    .credentialWithLink(email: emailAuth, emailLink: emailLink.toString());
try {
    await FirebaseAuth.instance.currentUser
        ?.reauthenticateWithCredential(authCredential);
} catch (error) {
    print("Error reauthenticating credential.");
}

但是,由于这个流程可能会在原始用户未登录的其他设备上结束,因此可能无法完成。在这种情况下,系统可以向用户显示错误,要求他们在同一设备上打开链接。部分状态可以在链接中传递,提供有关操作类型和用户 uid 的信息。

如果您同时支持基于密码和基于链接的电子邮件登录功能,为了区分密码/链接用户的登录方法,可以使用 fetchSignInMethodsForEmail。这对于标识符优先流程(首先要求用户提供他们的电子邮件地址,然后向用户显示登录方法)非常有用:

try {
    final signInMethods =
        await FirebaseAuth.instance.fetchSignInMethodsForEmail(emailAuth);
    final userExists = signInMethods.isNotEmpty;
    final canSignInWithLink = signInMethods
        .contains(EmailAuthProvider.EMAIL_LINK_SIGN_IN_METHOD);
    final canSignInWithPassword = signInMethods
        .contains(EmailAuthProvider.EMAIL_PASSWORD_SIGN_IN_METHOD);
} on FirebaseAuthException catch (exception) {
    switch (exception.code) {
        case "invalid-email":
            print("Not a valid email address.");
            break;
        default:
            print("Unknown error.");
    }
}

如上所述,电子邮件地址/密码和电子邮件/链接被认为是使用不同登录方法的相同 EmailAuthProviderPROVIDER_ID 相同)。

后续步骤

用户创建新帐号后,此帐号会存储为您的 Firebase 项目的一部分,无论用户使用哪种登录方法,您都可以使用此帐号在您项目中的每个应用中识别该用户。

在您的应用中,您可以从 User 对象获取用户的基本个人资料信息。请参阅管理用户

在您的 Firebase Realtime Database 和 Cloud Storage 安全规则中,您可以从 auth 变量获取已登录用户的唯一用户 ID,然后用其控制用户可以访问哪些数据。

您可以将身份验证服务提供方凭据关联至现有用户帐号,让用户可以使用多个身份验证服务提供方登录您的应用。

如需让用户退出登录,请调用 signOut()

await FirebaseAuth.instance.signOut();