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

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

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

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

准备工作

设置 Android 项目

  1. 将 Firebase 添加到您的 Android 项目(如果尚未添加)。

  2. 在您的模块(应用级)Gradle 文件(通常是 <project>/<app-module>/build.gradle.kts<project>/<app-module>/build.gradle)中,添加 Firebase Authentication Android 库的依赖项。我们建议使用 Firebase Android BoM 来实现库版本控制。

    此外,在设置 Firebase Authentication 时,您还需要将 Google Play 服务 SDK 添加到您的应用中。

    Kotlin+KTX

    dependencies {
        // Import the BoM for the Firebase platform
        implementation(platform("com.google.firebase:firebase-bom:32.3.1"))
    
        // Add the dependency for the Firebase Authentication library
        // When using the BoM, you don't specify versions in Firebase library dependencies
        implementation("com.google.firebase:firebase-auth-ktx")
    // Also add the dependency for the Google Play services library and specify its version implementation("com.google.android.gms:play-services-auth:20.7.0")
    }

    借助 Firebase Android BoM,可确保您的应用使用的始终是 Firebase Android 库的兼容版本。

    (替代方法) 在不使用 BoM 的情况下添加 Firebase 库依赖项

    如果您选择不使用 Firebase BoM,则必须在每个 Firebase 库的依赖项行中指定相应的库版本。

    请注意,如果您在应用中使用多个 Firebase 库,我们强烈建议您使用 BoM 来管理库版本,从而确保所有版本都兼容。

    dependencies {
        // Add the dependency for the Firebase Authentication library
        // When NOT using the BoM, you must specify versions in Firebase library dependencies
        implementation("com.google.firebase:firebase-auth-ktx:22.1.2")
    // Also add the dependency for the Google Play services library and specify its version implementation("com.google.android.gms:play-services-auth:20.7.0")
    }

    Java

    dependencies {
        // Import the BoM for the Firebase platform
        implementation(platform("com.google.firebase:firebase-bom:32.3.1"))
    
        // Add the dependency for the Firebase Authentication library
        // When using the BoM, you don't specify versions in Firebase library dependencies
        implementation("com.google.firebase:firebase-auth")
    // Also add the dependency for the Google Play services library and specify its version implementation("com.google.android.gms:play-services-auth:20.7.0")
    }

    借助 Firebase Android BoM,可确保您的应用使用的始终是 Firebase Android 库的兼容版本。

    (替代方法) 在不使用 BoM 的情况下添加 Firebase 库依赖项

    如果您选择不使用 Firebase BoM,则必须在每个 Firebase 库的依赖项行中指定相应的库版本。

    请注意,如果您在应用中使用多个 Firebase 库,我们强烈建议您使用 BoM 来管理库版本,从而确保所有版本都兼容。

    dependencies {
        // Add the dependency for the Firebase Authentication library
        // When NOT using the BoM, you must specify versions in Firebase library dependencies
        implementation("com.google.firebase:firebase-auth:22.1.2")
    // Also add the dependency for the Google Play services library and specify its version implementation("com.google.android.gms:play-services-auth:20.7.0")
    }

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

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

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

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

    • url:要嵌入的深层链接以及要传递的其他任何状态。您必须在 Firebase 控制台的“已获授权的网域”列表中添加链接的网域。您可以在“Sign in method”(登录方法)标签页(“Authentication”->“Sign in method”)中找到这些网域。如果用户的设备上尚未安装应用且过去无法安装应用,链接会将用户重定向到此网址。
    • androidPackageNameIOSBundleId:在 Android 或 Apple 设备上打开登录链接时使用的应用。详细了解如何配置 Firebase Dynamic Links,以通过移动应用打开电子邮件操作链接。
    • handleCodeInApp:设置为 true。与其他带外电子邮件操作(密码重置和电子邮件验证)不同,登录操作必须始终在应用中完成。这是因为按照预期,在流程结束时用户应该已经登录,并且他们的身份验证状态应在应用内持久保存。
    • dynamicLinkDomain:如果为项目定义了多个自定义动态链接网域,请选择在通过指定的移动应用打开链接时应使用其中哪个网域(例如 example.page.link)。如果不指定网域,系统会自动选择第一个网域。

    Kotlin+KTX

    val actionCodeSettings = 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
        setIOSBundleId("com.example.ios")
        setAndroidPackageName(
            "com.example.android",
            true, // installIfNotAvailable
            "12", // minimumVersion
        )
    }

    Java

    ActionCodeSettings actionCodeSettings =
            ActionCodeSettings.newBuilder()
                    // URL you want to redirect back to. The domain (www.example.com) for this
                    // URL must be whitelisted in the Firebase Console.
                    .setUrl("https://www.example.com/finishSignUp?cartId=1234")
                    // This must be true
                    .setHandleCodeInApp(true)
                    .setIOSBundleId("com.example.ios")
                    .setAndroidPackageName(
                            "com.example.android",
                            true, /* installIfNotAvailable */
                            "12"    /* minimumVersion */)
                    .build();

    如需详细了解 ActionCodeSettings,请参阅在电子邮件操作中传递状态部分的内容。

  2. 要求用户输入电子邮件地址。

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

    Kotlin+KTX

    Firebase.auth.sendSignInLinkToEmail(email, actionCodeSettings)
        .addOnCompleteListener { task ->
            if (task.isSuccessful) {
                Log.d(TAG, "Email sent.")
            }
        }

    Java

    FirebaseAuth auth = FirebaseAuth.getInstance();
    auth.sendSignInLinkToEmail(email, actionCodeSettings)
            .addOnCompleteListener(new OnCompleteListener<Void>() {
                @Override
                public void onComplete(@NonNull Task<Void> task) {
                    if (task.isSuccessful()) {
                        Log.d(TAG, "Email sent.");
                    }
                }
            });

安全考量

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

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

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

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

在 Android 应用中完成登录

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

在发送要在移动应用中打开的链接时,Firebase Auth 会使用 Firebase Dynamic Links。如需使用此功能,您必须在 Firebase 控制台中配置 Dynamic Links。

  1. 启用 Firebase Dynamic Links:

    1. Firebase 控制台中,打开 Dynamic Links 部分。
    2. 如果您尚未接受 Dynamic Links 条款,并且也未创建动态链接网域,请立即完成这些操作。

      如果您已经创建了动态链接网域,请记下此网域。动态链接网域通常采用如下格式:

      example.page.link

      配置 Apple 或 Android 应用以拦截传入链接时,您需要用到此值。

  2. 配置 Android 应用:

    1. 为了可以在 Android 应用中处理这些链接,需要在 Firebase 控制台项目设置中指定 Android 软件包名称。此外,您还需要提供应用证书的 SHA-1 和 SHA-256。
    2. 在您添加了动态链接网域并确保正确配置了 Android 应用后,动态链接将重定向到您的应用(从启动器 Activity 开始)。
    3. 如果您希望动态链接重定向到特定 Activity,则需要在 AndroidManifest.xml 文件中配置 intent 过滤器。为此,您可以在 intent 过滤器中指定动态链接网域或电子邮件操作处理程序。默认情况下,电子邮件操作处理程序托管在如下格式的网域上:
      PROJECT_ID.firebaseapp.com/
    4. 注意事项:
      1. 不要在 intent 过滤器中指定您在 actionCodeSettings 上设置的网址。
      2. 创建动态链接网域时,您可能还创建了一个短网址链接。系统不会传递这个短网址;请勿将您的 intent 过滤器配置为使用 android:pathPrefix 属性捕获该网址。如果这样做,您将无法在应用的不同部分捕获不同的动态链接。但是,您可以检查链接中的 mode 查询参数以了解系统正尝试执行哪项操作,或使用 SDK 方法(如 isSignInWithEmailLink)查看应用已收到的链接是否执行您预期的操作。
    5. 如需详细了解如何接收动态链接,请参阅接收动态链接 (Android) 的说明

在按上述说明收到链接后,验证链接确实用于电子邮件链接身份验证并完成登录。

Kotlin+KTX

val auth = Firebase.auth
val intent = intent
val emailLink = intent.data.toString()

// Confirm the link is a sign-in with email link.
if (auth.isSignInWithEmailLink(emailLink)) {
    // Retrieve this from wherever you stored it
    val email = "someemail@domain.com"

    // The client SDK will parse the code from the link for you.
    auth.signInWithEmailLink(email, emailLink)
        .addOnCompleteListener { task ->
            if (task.isSuccessful) {
                Log.d(TAG, "Successfully signed in with email link!")
                val result = task.result
                // You can access the new user via result.getUser()
                // Additional user info profile *not* available via:
                // result.getAdditionalUserInfo().getProfile() == null
                // You can check if the user is new or existing:
                // result.getAdditionalUserInfo().isNewUser()
            } else {
                Log.e(TAG, "Error signing in with email link", task.exception)
            }
        }
}

Java

FirebaseAuth auth = FirebaseAuth.getInstance();
Intent intent = getIntent();
String emailLink = intent.getData().toString();

// Confirm the link is a sign-in with email link.
if (auth.isSignInWithEmailLink(emailLink)) {
    // Retrieve this from wherever you stored it
    String email = "someemail@domain.com";

    // The client SDK will parse the code from the link for you.
    auth.signInWithEmailLink(email, emailLink)
            .addOnCompleteListener(new OnCompleteListener<AuthResult>() {
                @Override
                public void onComplete(@NonNull Task<AuthResult> task) {
                    if (task.isSuccessful()) {
                        Log.d(TAG, "Successfully signed in with email link!");
                        AuthResult result = task.getResult();
                        // You can access the new user via result.getUser()
                        // Additional user info profile *not* available via:
                        // result.getAdditionalUserInfo().getProfile() == null
                        // You can check if the user is new or existing:
                        // result.getAdditionalUserInfo().isNewUser()
                    } else {
                        Log.e(TAG, "Error signing in with email link", task.getException());
                    }
                }
            });
}

如需详细了解如何在 Apple 应用中处理使用电子邮件链接进行的登录,请参阅 Apple 平台指南

如需了解如何在 Web 应用中处理使用电子邮件链接的登录,请参阅 Web 指南

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

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

Kotlin+KTX

// Construct the email link credential from the current URL.
val credential = EmailAuthProvider.getCredentialWithLink(email, emailLink)

// Link the credential to the current user.
Firebase.auth.currentUser!!.linkWithCredential(credential)
    .addOnCompleteListener { task ->
        if (task.isSuccessful) {
            Log.d(TAG, "Successfully linked emailLink credential!")
            val result = task.result
            // You can access the new user via result.getUser()
            // Additional user info profile *not* available via:
            // result.getAdditionalUserInfo().getProfile() == null
            // You can check if the user is new or existing:
            // result.getAdditionalUserInfo().isNewUser()
        } else {
            Log.e(TAG, "Error linking emailLink credential", task.exception)
        }
    }

Java

// Construct the email link credential from the current URL.
AuthCredential credential =
        EmailAuthProvider.getCredentialWithLink(email, emailLink);

// Link the credential to the current user.
auth.getCurrentUser().linkWithCredential(credential)
        .addOnCompleteListener(new OnCompleteListener<AuthResult>() {
            @Override
            public void onComplete(@NonNull Task<AuthResult> task) {
                if (task.isSuccessful()) {
                    Log.d(TAG, "Successfully linked emailLink credential!");
                    AuthResult result = task.getResult();
                    // You can access the new user via result.getUser()
                    // Additional user info profile *not* available via:
                    // result.getAdditionalUserInfo().getProfile() == null
                    // You can check if the user is new or existing:
                    // result.getAdditionalUserInfo().isNewUser()
                } else {
                    Log.e(TAG, "Error linking emailLink credential", task.getException());
                }
            }
        });

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

Kotlin+KTX

// Construct the email link credential from the current URL.
val credential = EmailAuthProvider.getCredentialWithLink(email, emailLink)

// Re-authenticate the user with this credential.
Firebase.auth.currentUser!!.reauthenticateAndRetrieveData(credential)
    .addOnCompleteListener { task ->
        if (task.isSuccessful) {
            // User is now successfully reauthenticated
        } else {
            Log.e(TAG, "Error reauthenticating", task.exception)
        }
    }

Java

// Construct the email link credential from the current URL.
AuthCredential credential =
        EmailAuthProvider.getCredentialWithLink(email, emailLink);

// Re-authenticate the user with this credential.
auth.getCurrentUser().reauthenticateAndRetrieveData(credential)
        .addOnCompleteListener(new OnCompleteListener<AuthResult>() {
            @Override
            public void onComplete(@NonNull Task<AuthResult> task) {
                if (task.isSuccessful()) {
                    // User is now successfully reauthenticated
                } else {
                    Log.e(TAG, "Error reauthenticating", task.getException());
                }
            }
        });

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

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

Kotlin+KTX

Firebase.auth.fetchSignInMethodsForEmail(email)
    .addOnSuccessListener { result ->
        val signInMethods = result.signInMethods!!
        if (signInMethods.contains(EmailAuthProvider.EMAIL_PASSWORD_SIGN_IN_METHOD)) {
            // User can sign in with email/password
        } else if (signInMethods.contains(EmailAuthProvider.EMAIL_LINK_SIGN_IN_METHOD)) {
            // User can sign in with email/link
        }
    }
    .addOnFailureListener { exception ->
        Log.e(TAG, "Error getting sign in methods for user", exception)
    }

Java

auth.fetchSignInMethodsForEmail(email)
        .addOnCompleteListener(new OnCompleteListener<SignInMethodQueryResult>() {
            @Override
            public void onComplete(@NonNull Task<SignInMethodQueryResult> task) {
                if (task.isSuccessful()) {
                    SignInMethodQueryResult result = task.getResult();
                    List<String> signInMethods = result.getSignInMethods();
                    if (signInMethods.contains(EmailAuthProvider.EMAIL_PASSWORD_SIGN_IN_METHOD)) {
                        // User can sign in with email/password
                    } else if (signInMethods.contains(EmailAuthProvider.EMAIL_LINK_SIGN_IN_METHOD)) {
                        // User can sign in with email/link
                    }
                } else {
                    Log.e(TAG, "Error getting sign in methods for user", task.getException());
                }
            }
        });

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

后续步骤

在用户首次登录后,系统会创建一个新的用户帐号,并将其与该用户登录时使用的凭据(即用户名和密码、电话号码或者身份验证提供方信息)相关联。此新帐号存储在您的 Firebase 项目中,无论用户采用何种方式登录,您项目中的每个应用都可以使用此帐号来识别用户。

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

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

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

如需将用户退出登录,请调用 signOut

Kotlin+KTX

Firebase.auth.signOut()

Java

FirebaseAuth.getInstance().signOut();