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

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

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

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

准备工作

按照将 Firebase 添加到您的 JavaScript 项目中所述,从 Firebase 控制台将初始化代码段复制到您的项目(如果您尚未执行此操作)。

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

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

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

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

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

      Web 模块化 API

      const actionCodeSettings = {
        // URL you want to redirect back to. The domain (www.example.com) for this
        // URL must be in the authorized domains list in the Firebase Console.
        url: 'https://www.example.com/finishSignUp?cartId=1234',
        // This must be true.
        handleCodeInApp: true,
        iOS: {
          bundleId: 'com.example.ios'
        },
        android: {
          packageName: 'com.example.android',
          installApp: true,
          minimumVersion: '12'
        },
        dynamicLinkDomain: 'example.page.link'
      };

      Web 命名空间型 API

      var actionCodeSettings = {
        // URL you want to redirect back to. The domain (www.example.com) for this
        // URL must be in the authorized domains list in the Firebase Console.
        url: 'https://www.example.com/finishSignUp?cartId=1234',
        // This must be true.
        handleCodeInApp: true,
        iOS: {
          bundleId: 'com.example.ios'
        },
        android: {
          packageName: 'com.example.android',
          installApp: true,
          minimumVersion: '12'
        },
        dynamicLinkDomain: 'example.page.link'
      };

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

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

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

    Web 模块化 API

    import { getAuth, sendSignInLinkToEmail } from "firebase/auth";
    
    const auth = getAuth();
    sendSignInLinkToEmail(auth, email, actionCodeSettings)
      .then(() => {
        // The link was successfully sent. Inform the user.
        // Save the email locally so you don't need to ask the user for it again
        // if they open the link on the same device.
        window.localStorage.setItem('emailForSignIn', email);
        // ...
      })
      .catch((error) => {
        const errorCode = error.code;
        const errorMessage = error.message;
        // ...
      });

    Web 命名空间型 API

    firebase.auth().sendSignInLinkToEmail(email, actionCodeSettings)
      .then(() => {
        // The link was successfully sent. Inform the user.
        // Save the email locally so you don't need to ask the user for it again
        // if they open the link on the same device.
        window.localStorage.setItem('emailForSignIn', email);
        // ...
      })
      .catch((error) => {
        var errorCode = error.code;
        var errorMessage = error.message;
        // ...
      });

安全考量

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

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

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

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

在网页上完成登录

电子邮件链接深层链接的格式与带外电子邮件操作(电子邮件验证、密码重置和撤销电子邮件更改)所用的格式相同。Firebase Auth 提供 isSignInWithEmailLink API 来检查链接是否为通过电子邮件链接进行登录的链接,简化了此检查过程。

如需在着陆页上完成登录,请使用用户的电子邮件地址和包含一次性代码的实际电子邮件链接调用 signInWithEmailLink

Web 模块化 API

import { getAuth, isSignInWithEmailLink, signInWithEmailLink } from "firebase/auth";

// Confirm the link is a sign-in with email link.
const auth = getAuth();
if (isSignInWithEmailLink(auth, window.location.href)) {
  // Additional state parameters can also be passed via URL.
  // This can be used to continue the user's intended action before triggering
  // the sign-in operation.
  // Get the email if available. This should be available if the user completes
  // the flow on the same device where they started it.
  let email = window.localStorage.getItem('emailForSignIn');
  if (!email) {
    // User opened the link on a different device. To prevent session fixation
    // attacks, ask the user to provide the associated email again. For example:
    email = window.prompt('Please provide your email for confirmation');
  }
  // The client SDK will parse the code from the link for you.
  signInWithEmailLink(auth, email, window.location.href)
    .then((result) => {
      // Clear email from storage.
      window.localStorage.removeItem('emailForSignIn');
      // You can access the new user via result.user
      // Additional user info profile not available via:
      // result.additionalUserInfo.profile == null
      // You can check if the user is new or existing:
      // result.additionalUserInfo.isNewUser
    })
    .catch((error) => {
      // Some error occurred, you can inspect the code: error.code
      // Common errors could be invalid email and invalid or expired OTPs.
    });
}

Web 命名空间型 API

// Confirm the link is a sign-in with email link.
if (firebase.auth().isSignInWithEmailLink(window.location.href)) {
  // Additional state parameters can also be passed via URL.
  // This can be used to continue the user's intended action before triggering
  // the sign-in operation.
  // Get the email if available. This should be available if the user completes
  // the flow on the same device where they started it.
  var email = window.localStorage.getItem('emailForSignIn');
  if (!email) {
    // User opened the link on a different device. To prevent session fixation
    // attacks, ask the user to provide the associated email again. For example:
    email = window.prompt('Please provide your email for confirmation');
  }
  // The client SDK will parse the code from the link for you.
  firebase.auth().signInWithEmailLink(email, window.location.href)
    .then((result) => {
      // Clear email from storage.
      window.localStorage.removeItem('emailForSignIn');
      // You can access the new user via result.user
      // Additional user info profile not available via:
      // result.additionalUserInfo.profile == null
      // You can check if the user is new or existing:
      // result.additionalUserInfo.isNewUser
    })
    .catch((error) => {
      // Some error occurred, you can inspect the code: error.code
      // Common errors could be invalid email and invalid or expired OTPs.
    });
}

在移动应用中完成登录

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

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

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

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

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

Web 模块化 API

import { getAuth, linkWithCredential, EmailAuthProvider } from "firebase/auth";

// Construct the email link credential from the current URL.
const credential = EmailAuthProvider.credentialWithLink(
  email, window.location.href);

// Link the credential to the current user.
const auth = getAuth();
linkWithCredential(auth.currentUser, credential)
  .then((usercred) => {
    // The provider is now successfully linked.
    // The phone user can now sign in with their phone number or email.
  })
  .catch((error) => {
    // Some error occurred.
  });

Web 命名空间型 API

// Construct the email link credential from the current URL.
var credential = firebase.auth.EmailAuthProvider.credentialWithLink(
  email, window.location.href);

// Link the credential to the current user.
firebase.auth().currentUser.linkWithCredential(credential)
  .then((usercred) => {
    // The provider is now successfully linked.
    // The phone user can now sign in with their phone number or email.
  })
  .catch((error) => {
    // Some error occurred.
  });

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

Web 模块化 API

import { getAuth, reauthenticateWithCredential, EmailAuthProvider } from "firebase/auth";

// Construct the email link credential from the current URL.
const credential = EmailAuthProvider.credentialWithLink(
  email, window.location.href);

// Re-authenticate the user with this credential.
const auth = getAuth();
reauthenticateWithCredential(auth.currentUser, credential)
  .then((usercred) => {
    // The user is now successfully re-authenticated and can execute sensitive
    // operations.
  })
  .catch((error) => {
    // Some error occurred.
  });

Web 命名空间型 API

// Construct the email link credential from the current URL.
var credential = firebase.auth.EmailAuthProvider.credentialWithLink(
  email, window.location.href);

// Re-authenticate the user with this credential.
firebase.auth().currentUser.reauthenticateWithCredential(credential)
  .then((usercred) => {
    // The user is now successfully re-authenticated and can execute sensitive
    // operations.
  })
  .catch((error) => {
    // Some error occurred.
  });

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

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

Web 模块化 API

import { getAuth, fetchSignInMethodsForEmail, EmailAuthProvider} from "firebase/auth";

// After asking the user for their email.
const email = window.prompt('Please provide your email');

const auth = getAuth();
fetchSignInMethodsForEmail(auth, email)
  .then((signInMethods) => {
    // This returns the same array as fetchProvidersForEmail but for email
    // provider identified by 'password' string, signInMethods would contain 2
    // different strings:
    // 'emailLink' if the user previously signed in with an email/link
    // 'password' if the user has a password.
    // A user could have both.
    if (signInMethods.indexOf(EmailAuthProvider.EMAIL_PASSWORD_SIGN_IN_METHOD) != -1) {
      // User can sign in with email/password.
    }
    if (signInMethods.indexOf(EmailAuthProvider.EMAIL_LINK_SIGN_IN_METHOD) != -1) {
      // User can sign in with email/link.
    }
  })
  .catch((error) => {
    // Some error occurred, you can inspect the code: error.code
  });

Web 命名空间型 API

// After asking the user for their email.
var email = window.prompt('Please provide your email');
firebase.auth().fetchSignInMethodsForEmail(email)
  .then((signInMethods) => {
    // This returns the same array as fetchProvidersForEmail but for email
    // provider identified by 'password' string, signInMethods would contain 2
    // different strings:
    // 'emailLink' if the user previously signed in with an email/link
    // 'password' if the user has a password.
    // A user could have both.
    if (signInMethods.indexOf(
            firebase.auth.EmailAuthProvider.EMAIL_PASSWORD_SIGN_IN_METHOD) != -1) {
      // User can sign in with email/password.
    }
    if (signInMethods.indexOf(
            firebase.auth.EmailAuthProvider.EMAIL_LINK_SIGN_IN_METHOD) != -1) {
      // User can sign in with email/link.
    }
  })
  .catch((error) => {
    // Some error occurred, you can inspect the code: error.code
  });

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

后续步骤

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

  • 在您的应用中,建议通过在 Auth 对象上设置观测者 (observer) 来了解用户的身份验证状态。然后,您便可从 User 对象获取用户的基本个人资料信息。请参阅管理用户

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

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

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

Web 模块化 API

import { getAuth, signOut } from "firebase/auth";

const auth = getAuth();
signOut(auth).then(() => {
  // Sign-out successful.
}).catch((error) => {
  // An error happened.
});

Web 命名空间型 API

firebase.auth().signOut().then(() => {
  // Sign-out successful.
}).catch((error) => {
  // An error happened.
});