使用 JavaScript 中的電子郵件鏈接通過 Firebase 進行身份驗證

您可以使用 Firebase 身份驗證通過向用戶發送包含鏈接的電子郵件來登錄用戶,他們可以單擊該鏈接進行登錄。在此過程中,還會驗證用戶的電子郵件地址。

通過電子郵件登錄有很多好處:

  • 低摩擦註冊和登錄。
  • 降低跨應用程序重複使用密碼的風險,這可能會破壞即使是精心挑選的密碼的安全性。
  • 能夠驗證用戶身份,同時驗證用戶是否是電子郵件地址的合法所有者。
  • 用戶只需要一個可訪問的電子郵件帳戶即可登錄。不需要擁有電話號碼或社交媒體帳戶。
  • 用戶無需提供(或記住)密碼即可安全登錄,這在移動設備上可能很麻煩。
  • 以前使用電子郵件標識符(密碼或聯合身份)登錄的現有用戶可以升級為僅使用電子郵件登錄。例如,忘記密碼的用戶仍然可以登錄而無需重置密碼。

在你開始之前

如果您尚未將初始化代碼段從Firebase 控制台複製到您的項目中,請按照將Firebase 添加到 JavaScript 項目中的說明進行操作。

要通過電子郵件鏈接登錄用戶,您必須首先為您的 Firebase 項目啟用電子郵件提供商和電子郵件鏈接登錄方法:

  1. Firebase 控制台中,打開Auth部分。
  2. 登錄方法選項卡上,啟用電子郵件/密碼提供程序。請注意,必須啟用電子郵件/密碼登錄才能使用電子郵件鏈接登錄。
  3. 在同一部分中,啟用電子郵件鏈接(無密碼登錄)登錄方法。
  4. 單擊保存

要啟動身份驗證流程,請向用戶顯示一個界​​面,提示用戶提供他們的電子郵件地址,然後調用sendSignInLinkToEmail以請求 Firebase 將身份驗證鏈接發送到用戶的電子郵件。

  1. 構造ActionCodeSettings對象,該對象為 Firebase 提供有關如何構造電子郵件鏈接的說明。設置以下字段:

    • url :要嵌入的深層鏈接和要傳遞的任何其他狀態。必須將鏈接的域添加到授權域的 Firebase 控制台列表中,可以通過轉到登錄方法選項卡(身份驗證 -> 登錄方法)找到該列表。
    • androidios :在 Android 或 Apple 設備上打開登錄鏈接時使用的應用程序。詳細了解如何配置 Firebase 動態鏈接以通過移動應用打開電子郵件操作鏈接。
    • handleCodeInApp :設置為真。與其他帶外電子郵件操作(密碼重置和電子郵件驗證)不同,登錄操作必須始終在應用程序中完成。這是因為,在流程結束時,預計用戶會登錄,並且他們的身份驗證狀態會保留在應用程序中。
    • dynamicLinkDomain :當為一個項目定義了多個自定義動態鏈接域時,指定在通過指定的移動應用程序(例如example.page.link )打開鏈接時使用哪一個。否則將自動選擇第一個域。

      Web version 9

      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 version 8

      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 version 9

    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 version 8

    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 身份驗證要求在完成登錄流程時提供用戶的電子郵件地址。要成功登錄,此電子郵件地址必須與最初發送登錄鏈接的地址相匹配。

當您發送登錄電子郵件時,您可以通過在本地存儲他們的電子郵件地址(例如使用 localStorage 或 cookie)來為在請求鏈接的同一設備上打開登錄鏈接的用戶簡化此流程。然後,使用此地址完成流程。不要在重定向 URL 參數中傳遞用戶的電子郵件並重新使用它,因為這可能會啟用會話注入。

登錄完成後,任何以前未經驗證的登錄機制都將從用戶中刪除,任何現有會話都將失效。例如,如果有人之前使用相同的電子郵件和密碼創建了一個未經驗證的帳戶,則該用戶的密碼將被刪除,以防止聲稱擁有所有權並創建該未經驗證的帳戶的冒充者使用未經驗證的電子郵件和密碼再次登錄。

還要確保您在生產中使用 HTTPS URL,以避免您的鏈接可能被中間服務器攔截。

在網頁中完成登錄

電子郵件鏈接深層鏈接的格式與用於帶外電子郵件操作(電子郵件驗證、密碼重置和電子郵件更改撤銷)的格式相同。 Firebase Auth 通過提供isSignInWithEmailLink API 檢查鏈接是否為使用電子郵件登錄鏈接來簡化此檢查。

要在登錄頁面上完成登錄,請使用用戶的電子郵件和包含一次性代碼的實際電子郵件鏈接調用signInWithEmailLink

Web version 9

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 version 8

// 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 身份驗證使用 Firebase 動態鏈接將電子郵件鏈接發送到移動設備。對於通過移動應用程序完成登錄,必須將應用程序配置為檢測傳入的應用程序鏈接,解析底層深層鏈接,然後像通過 Web 流完成登錄一樣完成登錄。

要了解有關如何在 Android 應用程序中處理使用電子郵件鏈接登錄的更多信息,請參閱Android 指南

要了解更多關於如何在 Apple 應用程序中處理使用電子郵件鏈接登錄的信息,請參閱Apple 平台指南

您還可以將此身份驗證方法鏈接到現有用戶。例如,以前通過另一個提供商(例如電話號碼)進行身份驗證的用戶可以將此登錄方法添加到他們現有的帳戶中。

不同之處在於操作的後半部分:

Web version 9

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 version 8

// 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 version 9

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 version 8

// 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 version 9

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 version 8

// 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.EmailAuthProvider (相同PROVIDER_ID )。

下一步

用戶首次登錄後,會創建一個新用戶帳戶並將其鏈接到憑據(即用戶名和密碼、電話號碼或身份驗證提供商信息),即用戶登錄時使用的憑據。這個新帳戶作為 Firebase 項目的一部分存儲,可用於在項目中的每個應用中識別用戶,無論用戶如何登錄。

  • 在您的應用程序中,了解用戶身份驗證狀態的推薦方法是在Auth對像上設置觀察者。然後,您可以從User對像中獲取用戶的基本配置文件信息。請參閱管理用戶

  • 在您的 Firebase 實時數據庫和雲存儲安全規則中,您可以從auth變量中獲取登錄用戶的唯一用戶 ID,並使用它來控制用戶可以訪問哪些數據。

您可以通過將身份驗證提供程序憑據鏈接到現有用戶帳戶來允許用戶使用多個身份驗證提供程序登錄您的應用程序。

要註銷用戶,請調用signOut

Web version 9

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

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

Web version 8

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