向 Android 应用添加多重身份验证

本文档介绍如何向 Android 应用添加短信多重身份验证。

多重身份验证可提高应用的安全性。虽然攻击者通常会窃取密码和社交帐号,但拦截短信的难度要大得多。

准备工作

  1. 启用支持多重身份验证的提供商。其中包括:

    • 电子邮件和密码
    • 通过电子邮件发送链接
    • Google
    • Google Play
    • Facebook
    • Twitter
    • GitHub
    • Microsoft
    • Yahoo
    • 领英
  2. 确保您的应用正在验证用户的电子邮件。MFA 要求电子邮件验证。这可防止恶意操作者使用别人的电子邮件注册服务,然后通过添加第二重身份验证阻止真正的所有者登录。

  3. 在 Firebase 控制台中注册应用的 SHA-1 哈希(您的更改将自动转移到 Google Cloud Identity Platform)。

    1. 按照对客户端进行身份验证中的步骤获取应用的 SHA-1 哈希。

    2. 打开 Firebase 控制台

    3. 转到项目设置

    4. 您的应用 (Your apps) 下,点击 Android 图标。

    5. 按照引导步骤添加 SHA-1 哈希。

启用多重身份验证

  1. 转到 Cloud Console 中的 Identity Platform MFA 页面。
    转到 MFA 页面

  2. 在标题为基于短信的多重身份验证框中,点击启用

  3. 输入您将用于测试应用的电话号码。虽然并非必需,但我们强烈建议您注册测试电话号码,以免在开发过程中被节流。

  4. 如果您尚未授权应用的网域,请点击右侧的添加网域将其添加到允许列表中。

  5. 点击保存

选择注册模式

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

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

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

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

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

注册第二重身份验证

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

  1. 重新验证用户身份。

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

  3. 为用户获取多重身份验证会话:

    user.getMultiFactor().getSession()
       .addOnCompleteListener(
          new OnCompleteListener<MultiFactorSession>() {
           @Override
           public void onComplete(@NonNull Task<MultiFactorSession> task) {
             if (task.isSuccessful()) {
              MultiFactorSession multiFactorSession = task.getResult();
             }
           }
          });
    
  4. 构建一个 OnVerificationStateChangedCallbacks 对象以处理验证过程中的不同事件:

    OnVerificationStateChangedCallbacks callbacks =
     new OnVerificationStateChangedCallbacks() {
      @Override
      public void onVerificationCompleted(PhoneAuthCredential credential) {
        // This callback will be invoked in two situations:
        // 1) Instant verification. In some cases, the phone number can be
        //    instantly verified without needing to send or enter a verification
        //    code. You can disable this feature by calling
        //    PhoneAuthOptions.builder#requireSmsValidation(true) when building
        //    the options to pass to PhoneAuthProvider#verifyPhoneNumber().
        // 2) Auto-retrieval. On some devices, Google Play services can
        //    automatically detect the incoming verification SMS and perform
        //    verification without user action.
        this.credential = credential;
      }
      @Override
      public void onVerificationFailed(FirebaseException e) {
        // This callback is invoked in response to invalid requests for
        // verification, like an incorrect phone number.
        if (e instanceof FirebaseAuthInvalidCredentialsException) {
         // Invalid request
         // ...
        } else if (e instanceof FirebaseTooManyRequestsException) {
         // The SMS quota for the project has been exceeded
         // ...
        }
        // Show a message and update the UI
        // ...
      }
      @Override
      public void onCodeSent(
         String verificationId, PhoneAuthProvider.ForceResendingToken token) {
        // The SMS verification code has been sent to the provided phone number.
        // We now need to ask the user to enter the code and then construct a
        // credential by combining the code with a verification ID.
        // Save the verification ID and resending token for later use.
        this.verificationId = verificationId;
        this.forceResendingToken = token;
        // ...
      }
    };
    
  5. 使用用户的电话号码、多重身份验证会话和回调初始化 PhoneInfoOptions 对象:

    PhoneAuthOptions phoneAuthOptions =
       PhoneAuthOptions.newBuilder()
          .setPhoneNumber(phoneNumber)
          .setTimeout(30L, TimeUnit.SECONDS)
          .setMultiFactorSession(MultiFactorSession)
          .setCallbacks(callbacks)
          .build();
    

    默认情况下,系统会启用即时验证。要将其停用,请添加对 requireSmsValidation(true) 的调用。

  6. 向用户的手机发送验证消息:

    PhoneAuthProvider.verifyPhoneNumber(phoneAuthOptions);
    

    (可选)最佳做法是事先告知用户他们会收到短信,并需按标准费率支付短信费用。

  7. 短信验证码发出后,要求用户验证该验证码:

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

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

    // Complete enrollment. This will update the underlying tokens
    // and trigger ID token change listener.
    FirebaseAuth.getInstance()
       .getCurrentUser()
       .getMultiFactor()
       .enroll(multiFactorAssertion, "My personal phone number")
       .addOnCompleteListener(
          new OnCompleteListener<Void>() {
           @Override
           public void onComplete(@NonNull Task<Void> task) {
             // ...
           }
          });
    

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

MultiFactorAssertion multiFactorAssertion = PhoneMultiFactorGenerator.getAssertion(credential);
user.getMultiFactor().getSession()
   .addOnCompleteListener(
      new OnCompleteListener<MultiFactorSession>() {
       @Override
       public void onComplete(@NonNull Task<MultiFactorSession> task) {
         if (task.isSuccessful()) {
          MultiFactorSession multiFactorSession = task.getResult();
          PhoneAuthOptions phoneAuthOptions =
             PhoneAuthOptions.newBuilder()
                .setPhoneNumber(phoneNumber)
                .setTimeout(30L, TimeUnit.SECONDS)
                .setMultiFactorSession(multiFactorSession)
                .setCallbacks(callbacks)
                .build();
          // Send SMS verification code.
          PhoneAuthProvider.verifyPhoneNumber(phoneAuthOptions);
         }
       }
      });

// Ask user for the verification code.
PhoneAuthCredential credential =
   PhoneAuthProvider.getCredential(verificationId, verificationCode);

MultiFactorAssertion multiFactorAssertion = PhoneMultiFactorGenerator.getAssertion(credential);
// Complete enrollment.
FirebaseAuth.getInstance()
   .getCurrentUser()
   .getMultiFactor()
   .enroll(multiFactorAssertion, "My personal phone number")
   .addOnCompleteListener(
      new OnCompleteListener<Void>() {
       @Override
       public void onComplete(@NonNull Task<Void> task) {
         // ...
       }
      });

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

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

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

  1. 让用户以第一重身份验证登录,然后捕获 FirebaseAuthMultiFactorException 异常。此错误包含一个解析器,您可以使用该解析器获取用户注册的第二重身份验证。它还包含一个底层会话,可证明用户已成功通过其第一重身份验证。

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

    FirebaseAuth.getInstance()
       .signInWithEmailAndPassword(email, password)
       .addOnCompleteListener(
          new OnCompleteListener<AuthResult>() {
           @Override
           public void onComplete(@NonNull Task<AuthResult> task) {
             if (task.isSuccessful()) {
              // User is not enrolled with a second factor and is successfully
              // signed in.
              // ...
              return;
             }
             if (task.getException() instanceof FirebaseAuthMultiFactorException) {
              // The user is a multi-factor user. Second factor challenge is
              // required.
              MultiFactorResolver multiFactorResolver = task.getException().getResolver();
              // ...
             } else {
              // Handle other errors such as wrong password.
             }
           }
          });
    

    如果用户的第一重身份验证是联合提供商(例如 OAuth),则在调用 startActivityForSignInWithProvider() 后捕获错误。

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

    // Ask user which second factor to use.
    // You can get the masked phone number using
    // resolver.getHints().get(selectedIndex).getPhoneNumber()
    // You can get the display name using
    // resolver.getHints().get(selectedIndex).getDisplayName()
    if (resolver.getHints().get(selectedIndex).getFactorId()
       == PhoneMultiFactorGenerator.FACTOR_ID) {
     // User selected a phone second factor.
     MultiFactorInfo selectedHint =
       multiFactorResolver.getHints().get(selectedIndex);
    } else {
     // Unsupported second factor.
     // Note that only phone second factors are currently supported.
    }
    
  3. 使用提示和多重身份验证会话初始化 PhoneAuthOptions 对象。这些值包含在附加到 FirebaseAuthMultiFactorException 的解析器中。

    PhoneAuthOptions phoneAuthOptions =
       PhoneAuthOptions.newBuilder()
          .setMultiFactorHint(selectedHint)
          .setTimeout(30L, TimeUnit.SECONDS)
          .setMultiFactorSession(multiFactorResolver.getSession())
          .setCallbacks(callbacks)
          // Optionally disable instant verification.
          // .requireSmsValidation(true)
          .build();
    
  4. 向用户的手机发送验证消息:

    // Send SMS verification code
    PhoneAuthProvider.verifyPhoneNumber(phoneAuthOptions);
    
  5. 短信验证码发出后,要求用户验证该验证码:

    // Ask user for the verification code.
    PhoneAuthCredential credential
        = PhoneAuthProvider.getCredential(verificationId, verificationCode);
    
  6. 使用 PhoneAuthCredential 初始化 MultiFactorAssertion 对象:

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

    multiFactorResolver
       .resolveSignIn(multiFactorAssertion)
       .addOnCompleteListener(
          new OnCompleteListener<AuthResult>() {
           @Override
           public void onComplete(@NonNull Task<AuthResult> task) {
             if (task.isSuccessful()) {
              AuthResult authResult = task.getResult();
              // AuthResult will also contain the user, additionalUserInfo,
              // and an 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, authResult.getAdditionalUserInfo() will contain data
              // related to Google provider that the user signed in with.
              // authResult.getCredential() will contain the Google OAuth
              // credential.
              // authResult.getCredential().getAccessToken() will contain the
              // Google OAuth access token.
              // authResult.getCredential().getIdToken() contains the Google
              // OAuth ID token.
             }
           }
          });
    

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

FirebaseAuth.getInstance()
   .signInWithEmailAndPassword(email, password)
   .addOnCompleteListener(
      new OnCompleteListener<AuthResult>() {
       @Override
       public void onComplete(@NonNull Task<AuthResult> task) {
         if (task.isSuccessful()) {
          // User is not enrolled with a second factor and is successfully
          // signed in.
          // ...
          return;
         }
         if (task.getException() instanceof FirebaseAuthMultiFactorException) {
          FirebaseAuthMultiFactorException e =
            (FirebaseAuthMultiFactorException) task.getException();

          MultiFactorResolver multiFactorResolver = e.getResolver();

          // Ask user which second factor to use.
          MultiFactorInfo selectedHint =
             multiFactorResolver.getHints().get(selectedIndex);

          // Send the SMS verification code.
          PhoneAuthProvider.verifyPhoneNumber(
             PhoneAuthOptions.newBuilder()
                .setActivity(this)
                .setMultiFactorSession(multiFactorResolver.getSession())
                .setMultiFactorHint(selectedtHint)
                .setCallbacks(generateCallbacks())
                .setTimeout(30L, TimeUnit.SECONDS)
                .build());

          // Ask user for the SMS verification code.
          PhoneAuthCredential credential =
             PhoneAuthProvider.getCredential(verificationId, verificationCode);

          // Initialize a MultiFactorAssertion object with the
          // PhoneAuthCredential.
          MultiFactorAssertion multiFactorAssertion =
             PhoneMultiFactorGenerator.getAssertion(credential);

          // Complete sign-in.
          multiFactorResolver
             .resolveSignIn(multiFactorAssertion)
             .addOnCompleteListener(
                new OnCompleteListener<AuthResult>() {
                  @Override
                  public void onComplete(@NonNull Task<AuthResult> task) {
                   if (task.isSuccessful()) {
                     // User successfully signed in with the
                     // second factor phone number.
                   }
                   // ...
                  }
                });
         } else {
          // Handle other errors such as wrong password.
         }
       }
      });

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

后续步骤