在 JavaScript 中使用電子郵件連結進行 Firebase 驗證

您可以使用 Firebase 驗證功能,傳送含有連結的電子郵件給使用者,讓他們點選連結登入。在這個過程中,使用者的電子郵件地址也會經過驗證。

透過電子郵件登入有許多好處:

  • 註冊和登入流程簡便。
  • 降低跨應用程式重複使用密碼的風險,這可能會破壞精心挑選的密碼安全性。
  • 驗證使用者身分,同時確認使用者是否為電子郵件地址的合法擁有者。
  • 使用者只需要具備可存取的電子郵件帳戶即可登入。不必擁有電話號碼或社群媒體帳戶。
  • 使用者可以安全地登入,而無須提供 (或記住) 密碼,這在行動裝置上可能會很麻煩。
  • 如果使用者先前是使用電子郵件 ID (密碼或聯合登入) 登入,則可升級為只使用電子郵件登入。舉例來說,如果使用者忘記密碼,仍可登入帳戶,無須重設密碼。

事前準備

如果您尚未這麼做,請按照「 將 Firebase 新增至 JavaScript 專案」一文所述,將初始化程式碼片段從 Firebase 主控台複製到專案。

如要透過電子郵件連結登入使用者,您必須先為 Firebase 專案啟用電子郵件提供者和電子郵件連結登入方式:

  1. Firebase 控制台中,開啟「Auth」部分。
  2. 在「Sign in method」分頁中,啟用「Email/Password」提供者。請注意,您必須啟用電子郵件/密碼登入功能,才能使用電子郵件連結登入功能。
  3. 在同一個部分中,啟用「電子郵件連結 (不需要密碼即可登入)」登入方式。
  4. 按一下 [儲存]

如要啟動驗證流程,請向使用者顯示介面,提示使用者提供電子郵件地址,然後呼叫 sendSignInLinkToEmail,要求 Firebase 將驗證連結傳送至使用者的電子郵件地址。

  1. 建構 ActionCodeSettings 物件,為 Firebase 提供如何建構電子郵件連結的指示。設定下列欄位:

    • url:要嵌入的深層連結,以及要傳遞的任何其他狀態。您必須在 Firebase 控制台的授權網域清單中新增連結的網域,方法是前往「登入方式」分頁 (「驗證」->「設定」)。
    • androidios:協助 Firebase Authentication 判斷是否應建立在 Android 或 Apple 裝置上開啟的僅限網頁或行動連結。
    • handleCodeInApp:設為 true。與其他非頻道電子郵件操作 (密碼重設和電子郵件驗證) 不同,登入作業必須一律在應用程式中完成。這是因為在流程結束時,使用者應已登入,且其驗證狀態會在應用程式中保留。
    • linkDomain:為專案定義自訂 Hosting 連結網域時,請指定在特定行動應用程式開啟連結時要使用的網域。否則,系統會自動選取預設網域 (例如 PROJECT_ID.firebaseapp.com)。
    • dynamicLinkDomain:已淘汰。請勿指定此參數。

      Web

      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

      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

    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

    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 Auth 要求使用者在完成登入流程時提供電子郵件地址。如要順利登入,這個電子郵件地址必須與登入連結原先傳送的地址相符。

您可以為在要求連結時使用的相同裝置上開啟登入連結的使用者,在傳送登入電子郵件時,將電子郵件地址儲存在本機 (例如使用 localStorage 或 Cookie),藉此簡化這個流程。然後使用這個地址完成流程。請勿在重新導向網址參數中傳遞使用者的電子郵件,並重複使用,因為這可能會啟用工作階段注入。

登入完成後,系統會從使用者移除先前未經過驗證的登入機制,並將所有現有工作階段設為無效。舉例來說,如果有人先前使用相同的電子郵件地址和密碼建立未經驗證的帳戶,系統會移除使用者的密碼,以防冒用者聲稱擁有該帳戶,並使用未經驗證的電子郵件地址和密碼再次登入。

此外,請務必在實際工作環境中使用 HTTPS 網址,以免連結遭到中繼伺服器攔截。

在網頁中完成登入

電子郵件連結深層連結的格式與用於非頻道電子郵件動作 (電子郵件驗證、密碼重設和電子郵件變更撤銷) 的格式相同。Firebase Auth 提供 isSignInWithEmailLink API,可用於檢查連結是否為電子郵件登入連結,簡化這項檢查作業。

如要完成到達網頁上的登入程序,請使用使用者的電子郵件地址和含有一次性驗證碼的實際電子郵件連結,呼叫 signInWithEmailLink

Web

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 by importing getAdditionalUserInfo
      // and calling it with result:
      // getAdditionalUserInfo(result)
      // You can access the user's profile via:
      // getAdditionalUserInfo(result)?.profile
      // You can check if the user is new or existing:
      // getAdditionalUserInfo(result)?.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

// 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 Hosting 將電子郵件連結傳送至行動裝置。如要透過行動應用程式完成登入程序,應用程式必須經過設定,才能偵測傳入的應用程式連結、剖析底層深層連結,然後透過網路流程完成登入程序。

如要進一步瞭解如何在 Android 應用程式中處理透過電子郵件連結登入的作業,請參閱 Android 指南

如要進一步瞭解如何在 Apple 應用程式中使用電子郵件連結登入,請參閱 Apple 平台指南

您也可以將這種驗證方法連結至現有使用者。舉例來說,如果使用者先前透過其他提供者 (例如電話號碼) 進行驗證,便可將這類登入方式新增至現有帳戶。

差異在於運算的後半部:

Web

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

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

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

// 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 的相關資訊。

如果您是在 2023 年 9 月 15 日當天或之後建立專案,系統會預設啟用電子郵件列舉保護功能。這項功能可提升專案使用者帳戶的安全性,但會停用 fetchSignInMethodsForEmail() 方法,我們先前曾建議使用這項方法實作 ID 優先流程。

雖然您可以為專案停用電子郵件列舉防護,但我們不建議這麼做。

詳情請參閱電子郵件列舉防護功能的說明文件。

連結登入的預設電子郵件範本

預設電子郵件範本會在主旨和電子郵件內文中加入時間戳記,以免後續電子郵件會摺疊成單一會話串,並隱藏連結

這個範本適用於下列語言:

程式碼 語言
ar 阿拉伯文
zh-CN 簡體中文
zh-TW 繁體中文 (台灣)
nl 荷蘭文
en 英文
en-GB 英文 (英國)
fr 法文
de 德文
id 印尼文
it 義大利文
ja 日文
ko 韓文
pl 波蘭文
pt-BR 葡萄牙文 (巴西)
pt-PT 葡萄牙文 (葡萄牙)
ru 俄語
es 西班牙文
es-419 西班牙文 (拉丁美洲)
th 泰文

後續步驟

使用者首次登入後,系統會建立新使用者帳戶,並連結至使用者登入時所用的憑證 (即使用者名稱和密碼、電話號碼或驗證服務提供者資訊)。這個新帳戶會儲存在 Firebase 專案中,無論使用者如何登入,都可以用於在專案中的每個應用程式中識別使用者。

  • 在應用程式中,建議您在 Auth 物件上設定觀察器,以便瞭解使用者的驗證狀態。接著,您可以從 User 物件取得使用者的基本個人資料資訊。請參閱「管理使用者」。

  • Firebase Realtime DatabaseCloud Storage 安全性規則中,您可以從 auth 變數取得已登入使用者的專屬使用者 ID,並利用該 ID 控管使用者可存取的資料。

您可以將驗證服務供應商憑證連結至現有使用者帳戶,讓使用者使用多個驗證服務供應商登入應用程式。

如要將使用者登出,請呼叫 signOut

Web

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

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

Web

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