添加多重身份验证(Web 应用)

如果您已将项目升级为采用 Firebase Authentication with Identity Platform,则可以向 Web 应用添加短信多重身份验证。

多重身份验证可提高应用的安全性。虽然攻击者通常会窃取到密码和社交帐号,但截获短信却比较困难。

准备工作

  1. 请至少启用一个支持多重身份验证的提供方。每个提供方都支持 MFA,电话身份验证、匿名身份验证和 Apple Game Center 除外

  2. 确保您的应用会验证用户的电子邮件地址。MFA 要求验证电子邮件地址。这可防止图谋不轨者使用别人的电子邮件地址注册服务,然后通过添加第二重身份验证因素来阻止真正的电子邮件地址所有者注册。

使用多租户

如果要在多租户环境中启用多重身份验证,除了本文档中的其他说明,还需要完成以下步骤:

  1. 在 GCP 控制台中,选择要使用的租户。

  2. 在代码中,将 Auth 实例上的 tenantId 字段设置为租户的 ID。例如:

    Web 模块化 API

    import { getAuth } from "firebase/auth";
    
    const auth = getAuth(app);
    auth.tenantId = "myTenantId1";
    

    Web 命名空间型 API

    firebase.auth().tenantId = 'myTenantId1';
    

启用多重身份验证

  1. 打开 Firebase 控制台的 Authentication > 登录方法页面。

  2. 高级部分中,启用短信多重身份验证

    您还应输入要用于测试应用的电话号码。虽然并非必需,但我们强烈建议您注册测试电话号码,以免在开发过程中受到限制。

  3. 如果您尚未向应用网域授权,请在 Firebase 控制台的 Authentication > 设置页面上将其添加到允许列表中。

选择注册模式

您可以选择应用是否要求多重身份验证,以及何时和如何注册用户。一些常见模式包括:

  • 在注册过程中注册用户的第二重身份验证。如果应用要求所有用户进行多重身份验证,请使用此方法。

  • 提供可在注册期间跳过第二重身份验证注册的选项。如果应用鼓励但不要求进行多重身份验证,可以使用此方法。

  • 提供从用户的帐号或个人资料管理页面(而不是注册界面)添加第二重身份验证的功能。这样可以使注册过程更顺畅,同时仍可为注重安全的用户提供多重身份验证。

  • 如果用户希望访问安全性要求更高的功能,再要求添加第二重身份验证。

设置 reCAPTCHA 验证程序

您需要先配置 reCAPTCHA 验证程序,然后才能发送短信验证码。Firebase 使用 reCAPTCHA 来防止滥用行为,方法是确保电话号码验证请求来自应用允许的某个网域。

您无需手动设置 reCAPTCHA 客户端;客户端 SDK 的 RecaptchaVerifier 对象会自动创建并初始化任何必要的客户端密钥和密码。

使用隐形 reCAPTCHA

RecaptchaVerifier 对象支持隐形 reCAPTCHA,后者通常无需任何用户互动便能验证用户。如需使用隐形 reCAPTCHA,请创建一个 RecaptchaVerifier,将 size 参数设置为 invisible,并指定启动多重身份验证注册的界面元素的 ID:

Web 模块化 API

import { RecaptchaVerifier } from "firebase/auth";

const recaptchaVerifier = new RecaptchaVerifier("sign-in-button", {
    "size": "invisible",
    "callback": function(response) {
        // reCAPTCHA solved, you can proceed with
        // phoneAuthProvider.verifyPhoneNumber(...).
        onSolvedRecaptcha();
    }
}, auth);

Web 命名空间型 API

var recaptchaVerifier = new firebase.auth.RecaptchaVerifier('sign-in-button', {
'size': 'invisible',
'callback': function(response) {
  // reCAPTCHA solved, you can proceed with phoneAuthProvider.verifyPhoneNumber(...).
  onSolvedRecaptcha();
}
});

使用 reCAPTCHA widget

如需使用可见的 reCAPTCHA widget,请创建一个包含此 widget 的 HTML 元素,然后使用界面容器 ID 创建一个 RecaptchaVerifier 对象。您还可以选择设置在 reCAPTCHA 完成或过期时调用的回调:

Web 模块化 API

import { RecaptchaVerifier } from "firebase/auth";

const recaptchaVerifier = new RecaptchaVerifier(
    "recaptcha-container",

    // Optional reCAPTCHA parameters.
    {
      "size": "normal",
      "callback": function(response) {
        // reCAPTCHA solved, you can proceed with
        // phoneAuthProvider.verifyPhoneNumber(...).
        onSolvedRecaptcha();
      },
      "expired-callback": function() {
        // Response expired. Ask user to solve reCAPTCHA again.
        // ...
      }
    }, auth
);

Web 命名空间型 API

var recaptchaVerifier = new firebase.auth.RecaptchaVerifier(
  'recaptcha-container',
  // Optional reCAPTCHA parameters.
  {
    'size': 'normal',
    'callback': function(response) {
      // reCAPTCHA solved, you can proceed with phoneAuthProvider.verifyPhoneNumber(...).
      // ...
      onSolvedRecaptcha();
    },
    'expired-callback': function() {
      // Response expired. Ask user to solve reCAPTCHA again.
      // ...
    }
  });

预呈现 reCAPTCHA

(可选)您可以在启动双重身份验证注册之前预呈现 reCAPTCHA:

Web 模块化 API

recaptchaVerifier.render()
    .then(function (widgetId) {
        window.recaptchaWidgetId = widgetId;
    });

Web 命名空间型 API

recaptchaVerifier.render()
  .then(function(widgetId) {
    window.recaptchaWidgetId = widgetId;
  });

render() 解析完成后,您会获得 reCAPTCHA 的 widget ID,此 ID 可用于调用 reCAPTCHA API

var recaptchaResponse = grecaptcha.getResponse(window.recaptchaWidgetId);

RecaptchaVerifier 通过 verify 方法将此逻辑抽象化,因此您无需直接处理 grecaptcha 变量。

注册第二重身份验证

如需为用户注册新的第二重身份验证,请执行以下操作:

  1. 重新验证用户身份。

  2. 让用户输入自己的电话号码。

  3. 按照上一部分中的说明,初始化 reCAPTCHA 验证程序。如果已经配置了 RecaptchaVerifier 实例,请跳过此步骤:

    Web 模块化 API

    import { RecaptchaVerifier } from "firebase/auth";
    
    const recaptchaVerifier = new RecaptchaVerifier('recaptcha-container-id', undefined, auth);
    

    Web 命名空间型 API

    var recaptchaVerifier = new firebase.auth.RecaptchaVerifier('recaptcha-container-id');
    
  4. 为用户获取多重身份验证会话:

    Web 模块化 API

    import { multiFactor } from "firebase/auth";
    
    multiFactor(user).getSession().then(function (multiFactorSession) {
        // ...
    });
    

    Web 命名空间型 API

    user.multiFactor.getSession().then(function(multiFactorSession) {
      // ...
    })
    
  5. 使用用户的电话号码和多重身份验证会话初始化 PhoneInfoOptions 对象:

    Web 模块化 API

    // Specify the phone number and pass the MFA session.
    const phoneInfoOptions = {
      phoneNumber: phoneNumber,
      session: multiFactorSession
    };
    

    Web 命名空间型 API

    // Specify the phone number and pass the MFA session.
    var phoneInfoOptions = {
      phoneNumber: phoneNumber,
      session: multiFactorSession
    };
    
  6. 向用户的电话发送验证消息:

    Web 模块化 API

    import { PhoneAuthProvider } from "firebase/auth";
    
    const phoneAuthProvider = new PhoneAuthProvider(auth);
    phoneAuthProvider.verifyPhoneNumber(phoneInfoOptions, recaptchaVerifier)
        .then(function (verificationId) {
            // verificationId will be needed to complete enrollment.
        });
    

    Web 命名空间型 API

    var phoneAuthProvider = new firebase.auth.PhoneAuthProvider();
    // Send SMS verification code.
    return phoneAuthProvider.verifyPhoneNumber(phoneInfoOptions, recaptchaVerifier)
      .then(function(verificationId) {
        // verificationId will be needed for enrollment completion.
      })
    

    虽然没有强制要求,但最好事先告知用户他们会收到短信,并需按标准费率支付短信费用。

  7. 如果请求失败,请重置 reCAPTCHA,然后重复执行上一步,以便用户重试。请注意,verifyPhoneNumber() 在发生错误时会自动重置 reCAPTCHA,因为 reCAPTCHA 令牌是一次性的。

    Web 模块化 API

    recaptchaVerifier.clear();
    

    Web 命名空间型 API

    recaptchaVerifier.clear();
    
  8. 短信验证码发出后,要求用户验证该验证码:

    Web 模块化 API

    // Ask user for the verification code. Then:
    const cred = PhoneAuthProvider.credential(verificationId, verificationCode);
    

    Web 命名空间型 API

    // Ask user for the verification code. Then:
    var cred = firebase.auth.PhoneAuthProvider.credential(verificationId, verificationCode);
    
  9. 使用 PhoneAuthCredential 初始化 MultiFactorAssertion 对象:

    Web 模块化 API

    import { PhoneMultiFactorGenerator } from "firebase/auth";
    
    const multiFactorAssertion = PhoneMultiFactorGenerator.assertion(cred);
    

    Web 命名空间型 API

    var multiFactorAssertion = firebase.auth.PhoneMultiFactorGenerator.assertion(cred);
    
  10. 完成注册。(可选)您可以为第二重身份验证指定显示名称。这对于具有多个第二重身份验证的用户非常有用,因为电话号码在身份验证流程中会被遮盖(例如 +1******1234)。

    Web 模块化 API

    // Complete enrollment. This will update the underlying tokens
    // and trigger ID token change listener.
    multiFactor(user).enroll(multiFactorAssertion, "My personal phone number");
    

    Web 命名空间型 API

    // Complete enrollment. This will update the underlying tokens
    // and trigger ID token change listener.
    user.multiFactor.enroll(multiFactorAssertion, 'My personal phone number');
    

以下代码展示了注册第二重身份验证的完整示例:

Web 模块化 API

import {
    multiFactor, PhoneAuthProvider, PhoneMultiFactorGenerator,
    RecaptchaVerifier
} from "firebase/auth";

const recaptchaVerifier = new RecaptchaVerifier('recaptcha-container-id', undefined, auth);
multiFactor(user).getSession()
    .then(function (multiFactorSession) {
        // Specify the phone number and pass the MFA session.
        const phoneInfoOptions = {
            phoneNumber: phoneNumber,
            session: multiFactorSession
        };

        const phoneAuthProvider = new PhoneAuthProvider(auth);

        // Send SMS verification code.
        return phoneAuthProvider.verifyPhoneNumber(phoneInfoOptions, recaptchaVerifier);
    }).then(function (verificationId) {
        // Ask user for the verification code. Then:
        const cred = PhoneAuthProvider.credential(verificationId, verificationCode);
        const multiFactorAssertion = PhoneMultiFactorGenerator.assertion(cred);

        // Complete enrollment.
        return multiFactor(user).enroll(multiFactorAssertion, mfaDisplayName);
    });

Web 命名空间型 API

var recaptchaVerifier = new firebase.auth.RecaptchaVerifier('recaptcha-container-id');
user.multiFactor.getSession().then(function(multiFactorSession) {
  // Specify the phone number and pass the MFA session.
  var phoneInfoOptions = {
    phoneNumber: phoneNumber,
    session: multiFactorSession
  };
  var phoneAuthProvider = new firebase.auth.PhoneAuthProvider();
  // Send SMS verification code.
  return phoneAuthProvider.verifyPhoneNumber(
      phoneInfoOptions, recaptchaVerifier);
})
.then(function(verificationId) {
  // Ask user for the verification code.
  var cred = firebase.auth.PhoneAuthProvider.credential(verificationId, verificationCode);
  var multiFactorAssertion = firebase.auth.PhoneMultiFactorGenerator.assertion(cred);
  // Complete enrollment.
  return user.multiFactor.enroll(multiFactorAssertion, mfaDisplayName);
});

恭喜!您已成功为用户注册了第二重身份验证。

让用户通过第二重身份验证登录

如需让用户通过双重身份验证(包含短信验证)登录,请执行以下操作:

  1. 让用户通过第一重身份验证登录,然后捕获 auth/multi-factor-auth-required 错误。此错误包含一个解析器、有关已注册的第二重身份验证的提示,以及一个证明用户已成功通过第一重身份验证的底层会话。

    例如,如果用户的第一重身份验证是电子邮件地址和密码:

    Web 模块化 API

    import { getAuth, getMultiFactorResolver} from "firebase/auth";
    
    const auth = getAuth();
    signInWithEmailAndPassword(auth, email, password)
        .then(function (userCredential) {
            // User successfully signed in and is not enrolled with a second factor.
        })
        .catch(function (error) {
            if (error.code == 'auth/multi-factor-auth-required') {
                // The user is a multi-factor user. Second factor challenge is required.
                resolver = getMultiFactorResolver(auth, error);
                // ...
            } else if (error.code == 'auth/wrong-password') {
                // Handle other errors such as wrong password.
            }
    });
    

    Web 命名空间型 API

    firebase.auth().signInWithEmailAndPassword(email, password)
      .then(function(userCredential) {
        // User successfully signed in and is not enrolled with a second factor.
      })
      .catch(function(error) {
        if (error.code == 'auth/multi-factor-auth-required') {
          // The user is a multi-factor user. Second factor challenge is required.
          resolver = error.resolver;
          // ...
        } else if (error.code == 'auth/wrong-password') {
          // Handle other errors such as wrong password.
        } ...
      });
    

    如果用户的第一重身份验证是联合提供方(例如 OAuth、SAML 或 OIDC),请在调用 signInWithPopup()signInWithRedirect() 后捕获错误。

  2. 如果用户注册了多个第二重身份验证,请询问用户要使用哪一个:

    Web 模块化 API

    // Ask user which second factor to use.
    // You can get the masked phone number via resolver.hints[selectedIndex].phoneNumber
    // You can get the display name via resolver.hints[selectedIndex].displayName
    
    if (resolver.hints[selectedIndex].factorId ===
        PhoneMultiFactorGenerator.FACTOR_ID) {
        // User selected a phone second factor.
        // ...
    } else if (resolver.hints[selectedIndex].factorId ===
               TotpMultiFactorGenerator.FACTOR_ID) {
        // User selected a TOTP second factor.
        // ...
    } else {
        // Unsupported second factor.
    }
    

    Web 命名空间型 API

    // Ask user which second factor to use.
    // You can get the masked phone number via resolver.hints[selectedIndex].phoneNumber
    // You can get the display name via resolver.hints[selectedIndex].displayName
    if (resolver.hints[selectedIndex].factorId === firebase.auth.PhoneMultiFactorGenerator.FACTOR_ID) {
      // User selected a phone second factor.
      // ...
    } else if (resolver.hints[selectedIndex].factorId === firebase.auth.TotpMultiFactorGenerator.FACTOR_ID) {
      // User selected a TOTP second factor.
      // ...
    } else {
      // Unsupported second factor.
    }
    
  3. 按照上一部分中的说明,初始化 reCAPTCHA 验证程序。如果已经配置了 RecaptchaVerifier 实例,请跳过此步骤:

    Web 模块化 API

    import { RecaptchaVerifier } from "firebase/auth";
    
    recaptchaVerifier = new RecaptchaVerifier('recaptcha-container-id', undefined, auth);
    

    Web 命名空间型 API

    var recaptchaVerifier = new firebase.auth.RecaptchaVerifier('recaptcha-container-id');
    
  4. 使用用户的电话号码和多重身份验证会话初始化 PhoneInfoOptions 对象。这些值包含在传递给 auth/multi-factor-auth-required 错误的 resolver 对象中:

    Web 模块化 API

    const phoneInfoOptions = {
        multiFactorHint: resolver.hints[selectedIndex],
        session: resolver.session
    };
    

    Web 命名空间型 API

    var phoneInfoOptions = {
      multiFactorHint: resolver.hints[selectedIndex],
      session: resolver.session
    };
    
  5. 向用户的电话发送验证消息:

    Web 模块化 API

    // Send SMS verification code.
    const phoneAuthProvider = new PhoneAuthProvider(auth);
    phoneAuthProvider.verifyPhoneNumber(phoneInfoOptions, recaptchaVerifier)
        .then(function (verificationId) {
            // verificationId will be needed for sign-in completion.
        });
    

    Web 命名空间型 API

    var phoneAuthProvider = new firebase.auth.PhoneAuthProvider();
    // Send SMS verification code.
    return phoneAuthProvider.verifyPhoneNumber(phoneInfoOptions, recaptchaVerifier)
      .then(function(verificationId) {
        // verificationId will be needed for sign-in completion.
      })
    
  6. 如果请求失败,请重置 reCAPTCHA,然后重复执行上一步,以便用户重试:

    Web 模块化 API

    recaptchaVerifier.clear();
    

    Web 命名空间型 API

    recaptchaVerifier.clear();
    
  7. 短信验证码发出后,要求用户验证该验证码:

    Web 模块化 API

    const cred = PhoneAuthProvider.credential(verificationId, verificationCode);
    

    Web 命名空间型 API

    // Ask user for the verification code. Then:
    var cred = firebase.auth.PhoneAuthProvider.credential(verificationId, verificationCode);
    
  8. 使用 PhoneAuthCredential 初始化 MultiFactorAssertion 对象:

    Web 模块化 API

    const multiFactorAssertion = PhoneMultiFactorGenerator.assertion(cred);
    

    Web 命名空间型 API

    var multiFactorAssertion = firebase.auth.PhoneMultiFactorGenerator.assertion(cred);
    
  9. 调用 resolver.resolveSignIn() 以完成第二重身份验证。然后,您可以访问原始登录结果,其中包括特定于提供方的标准数据和身份验证凭据:

    Web 模块化 API

    // Complete sign-in. This will also trigger the Auth state listeners.
    resolver.resolveSignIn(multiFactorAssertion)
        .then(function (userCredential) {
            // userCredential will also contain the user, additionalUserInfo, optional
            // credential (null for email/password) associated with the first factor sign-in.
    
            // For example, if the user signed in with Google as a first factor,
            // userCredential.additionalUserInfo will contain data related to Google
            // provider that the user signed in with.
            // - user.credential contains the Google OAuth credential.
            // - user.credential.accessToken contains the Google OAuth access token.
            // - user.credential.idToken contains the Google OAuth ID token.
        });
    

    Web 命名空间型 API

    // Complete sign-in. This will also trigger the Auth state listeners.
    resolver.resolveSignIn(multiFactorAssertion)
      .then(function(userCredential) {
        // userCredential will also contain the user, additionalUserInfo, optional
        // credential (null for email/password) associated with the first factor sign-in.
        // For example, if the user signed in with Google as a first factor,
        // userCredential.additionalUserInfo will contain data related to Google provider that
        // the user signed in with.
        // user.credential contains the Google OAuth credential.
        // user.credential.accessToken contains the Google OAuth access token.
        // user.credential.idToken contains the Google OAuth ID token.
      });
    

以下代码展示了让多重身份验证用户登录的完整示例:

Web 模块化 API

import {
    getAuth,
    getMultiFactorResolver,
    PhoneAuthProvider,
    PhoneMultiFactorGenerator,
    RecaptchaVerifier,
    signInWithEmailAndPassword
} from "firebase/auth";

const recaptchaVerifier = new RecaptchaVerifier('recaptcha-container-id', undefined, auth);

const auth = getAuth();
signInWithEmailAndPassword(auth, email, password)
    .then(function (userCredential) {
        // User is not enrolled with a second factor and is successfully
        // signed in.
        // ...
    })
    .catch(function (error) {
        if (error.code == 'auth/multi-factor-auth-required') {
            const resolver = getMultiFactorResolver(auth, error);
            // Ask user which second factor to use.
            if (resolver.hints[selectedIndex].factorId ===
                PhoneMultiFactorGenerator.FACTOR_ID) {
                const phoneInfoOptions = {
                    multiFactorHint: resolver.hints[selectedIndex],
                    session: resolver.session
                };
                const phoneAuthProvider = new PhoneAuthProvider(auth);
                // Send SMS verification code
                return phoneAuthProvider.verifyPhoneNumber(phoneInfoOptions, recaptchaVerifier)
                    .then(function (verificationId) {
                        // Ask user for the SMS verification code. Then:
                        const cred = PhoneAuthProvider.credential(
                            verificationId, verificationCode);
                        const multiFactorAssertion =
                            PhoneMultiFactorGenerator.assertion(cred);
                        // Complete sign-in.
                        return resolver.resolveSignIn(multiFactorAssertion)
                    })
                    .then(function (userCredential) {
                        // User successfully signed in with the second factor phone number.
                    });
            } else if (resolver.hints[selectedIndex].factorId ===
                       TotpMultiFactorGenerator.FACTOR_ID) {
                // Handle TOTP MFA.
                // ...
            } else {
                // Unsupported second factor.
            }
        } else if (error.code == 'auth/wrong-password') {
            // Handle other errors such as wrong password.
        }
    });

Web 命名空间型 API

var resolver;
firebase.auth().signInWithEmailAndPassword(email, password)
  .then(function(userCredential) {
    // User is not enrolled with a second factor and is successfully signed in.
    // ...
  })
  .catch(function(error) {
    if (error.code == 'auth/multi-factor-auth-required') {
      resolver = error.resolver;
      // Ask user which second factor to use.
      if (resolver.hints[selectedIndex].factorId ===
          firebase.auth.PhoneMultiFactorGenerator.FACTOR_ID) {
        var phoneInfoOptions = {
          multiFactorHint: resolver.hints[selectedIndex],
          session: resolver.session
        };
        var phoneAuthProvider = new firebase.auth.PhoneAuthProvider();
        // Send SMS verification code
        return phoneAuthProvider.verifyPhoneNumber(phoneInfoOptions, recaptchaVerifier)
          .then(function(verificationId) {
            // Ask user for the SMS verification code.
            var cred = firebase.auth.PhoneAuthProvider.credential(
                verificationId, verificationCode);
            var multiFactorAssertion =
                firebase.auth.PhoneMultiFactorGenerator.assertion(cred);
            // Complete sign-in.
            return resolver.resolveSignIn(multiFactorAssertion)
          })
          .then(function(userCredential) {
            // User successfully signed in with the second factor phone number.
          });
      } else if (resolver.hints[selectedIndex].factorId ===
        firebase.auth.TotpMultiFactorGenerator.FACTOR_ID) {
        // Handle TOTP MFA.
        // ...
      } else {
        // Unsupported second factor.
      }
    } else if (error.code == 'auth/wrong-password') {
      // Handle other errors such as wrong password.
    } ...
  });

恭喜!您已成功让使用多重身份验证的用户登录。

后续步骤