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

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

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

准备工作

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

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

启用多重身份验证

  1. 前往 Google Cloud 控制台中的 Identity Platform MFA 页面。
    转到 MFA 页面

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

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

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

  5. 点击保存

验证应用

Identity Platform 需要验证短信请求是否来自您的应用。您可以通过以下两种方法实现此目的:

  • 静默 APNs 通知:用户首次登录时,Identity Platform 可以向用户的设备发送静默推送通知。如果应用收到通知,则可以继续进行身份验证。请注意,从 iOS 8.0 开始,您无需要求用户允许推送通知即可使用此方法。

  • reCAPTCHA 验证:如果您无法发送静默通知(例如,由于用户停用了后台刷新,或者您在 iOS 模拟器中测试应用),则可以使用 reCAPTCHA。在许多情况下,reCAPTCHA 会自动完成,无需与用户互动。

使用静默通知

启用 APNs 通知以用于 Identity Platform:

  1. 在 Xcode 中为您的项目启用推送通知

  2. 使用 Firebase 控制台上传您的 APNs 身份验证密钥(您的更改将自动转移到 Google Cloud Identity Platform)。 如果您还没有 APNs 身份验证密钥,请参阅配置 FCM APNs,了解如何获取密钥。

    1. 打开 Firebase 控制台

    2. 前往项目设置

    3. 选择 Cloud Messaging 标签页。

    4. iOS 应用配置部分的 APNs 身份验证密钥下,点击上传

    5. 选择密钥。

    6. 添加该密钥的 ID。您可以在 Apple Developer Member Center 中的 Certificates, Identifiers & Profiles(证书、标识符和个人资料)下找到密钥 ID。

    7. 点击上传

如果您已有 APNs 证书,可以改为上传该证书。

使用 reCAPTCHA 验证

如需让客户端 SDK 能够使用 reCAPTCHA,请执行以下操作:

  1. 在 Xcode 中打开您的项目配置。

  2. 在左侧树状视图中,双击项目名称。

  3. 目标部分中选择您的应用。

  4. 选择信息标签页。

  5. 展开网址类型部分。

  6. 点击 + 按钮。

  7. 网址方案字段中输入您的倒序客户端 ID。您可以在 GoogleService-Info.plist 配置文件中找到列为 REVERSED_CLIENT_ID 的此值。

完成后,您的配置应类似如下内容:

自定义方案

(可选)您可以自定义应用在显示 reCAPTCHA 时呈现 SFSafariViewControllerUIWebView 的方式。为此,请创建符合 FIRAuthUIDelegate 协议的自定义类,并将其传递给 verifyPhoneNumber:UIDelegate:completion:

选择注册模式

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

  • 在注册过程中注册用户的第二重身份验证。如果应用要求所有用户进行多重身份验证,请使用此方法。请注意,账号必须具有经过验证的电子邮件地址才能注册第二重身份验证,您的注册流程必须考虑到这一点。

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

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

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

注册第二重身份验证

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

  1. 重新验证用户身份。

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

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

    Swift

    authResult.user.multiFactor.getSessionWithCompletion() { (session, error) in
      // ...
    }
    

    Objective-C

    [authResult.user.multiFactor
      getSessionWithCompletion:^(FIRMultiFactorSession * _Nullable session,
                                NSError * _Nullable error) {
        // ...
    }];
    
  4. 向用户的电话发送验证消息。确保电话号码的格式准确无误,即具有 + 前缀,并且没有其他标点符号或空格(例如:+15105551234

    Swift

    // Send SMS verification code.
    PhoneAuthProvider.provider().verifyPhoneNumber(
      phoneNumber,
      uiDelegate: nil,
      multiFactorSession: session) { (verificationId, error) in
        // verificationId will be needed for enrollment completion.
    }
    

    Objective-C

    // Send SMS verification code.
    [FIRPhoneAuthProvider.provider verifyPhoneNumber:phoneNumber
                                          UIDelegate:nil
                                  multiFactorSession:session
                                          completion:^(NSString * _Nullable verificationID,
                                                        NSError * _Nullable error) {
        // verificationId will be needed for enrollment completion.
    }];
    

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

    verifyPhoneNumber() 方法会使用静默推送通知在后台启动应用验证流程。如果静默推送通知不可用,系统会改为进行 reCAPTCHA 验证。

  5. 短信验证码发出后,要求用户验证该验证码。然后,使用其响应来构建 PhoneAuthCredential

    Swift

    // Ask user for the verification code. Then:
    let credential = PhoneAuthProvider.provider().credential(
      withVerificationID: verificationId,
      verificationCode: verificationCode)
    

    Objective-C

    // Ask user for the SMS verification code. Then:
    FIRPhoneAuthCredential *credential = [FIRPhoneAuthProvider.provider
                                           credentialWithVerificationID:verificationID
                                           verificationCode:kPhoneSecondFactorVerificationCode];
    
  6. 初始化断言对象:

    Swift

    let assertion = PhoneMultiFactorGenerator.assertion(with: credential)
    

    Objective-C

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

    Swift

    // Complete enrollment. This will update the underlying tokens
    // and trigger ID token change listener.
    user.multiFactor.enroll(with: assertion, displayName: displayName) { (error) in
      // ...
    }
    

    Objective-C

    // Complete enrollment. This will update the underlying tokens
    // and trigger ID token change listener.
    [authResult.user.multiFactor enrollWithAssertion:assertion
                                         displayName:nil
                                          completion:^(NSError * _Nullable error) {
        // ...
    }];
    

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

Swift

let user = Auth.auth().currentUser
user?.multiFactor.getSessionWithCompletion({ (session, error) in
  // Send SMS verification code.
  PhoneAuthProvider.provider().verifyPhoneNumber(
    phoneNumber,
    uiDelegate: nil,
    multiFactorSession: session
  ) { (verificationId, error) in
    // verificationId will be needed for enrollment completion.
    // Ask user for the verification code.
    let credential = PhoneAuthProvider.provider().credential(
      withVerificationID: verificationId!,
      verificationCode: phoneSecondFactorVerificationCode)
    let assertion = PhoneMultiFactorGenerator.assertion(with: credential)
    // Complete enrollment. This will update the underlying tokens
    // and trigger ID token change listener.
    user?.multiFactor.enroll(with: assertion, displayName: displayName) { (error) in
      // ...
    }
  }
})

Objective-C

FIRUser *user = FIRAuth.auth.currentUser;
[user.multiFactor getSessionWithCompletion:^(FIRMultiFactorSession * _Nullable session,
                                              NSError * _Nullable error) {
    // Send SMS verification code.
    [FIRPhoneAuthProvider.provider
      verifyPhoneNumber:phoneNumber
      UIDelegate:nil
      multiFactorSession:session
      completion:^(NSString * _Nullable verificationID, NSError * _Nullable error) {
        // verificationId will be needed for enrollment completion.

        // Ask user for the verification code.
        // ...

        // Then:
        FIRPhoneAuthCredential *credential =
            [FIRPhoneAuthProvider.provider credentialWithVerificationID:verificationID
                                                        verificationCode:kPhoneSecondFactorVerificationCode];
        FIRMultiFactorAssertion *assertion =
            [FIRPhoneMultiFactorGenerator assertionWithCredential:credential];

        // Complete enrollment. This will update the underlying tokens
        // and trigger ID token change listener.
        [user.multiFactor enrollWithAssertion:assertion
                                  displayName:displayName
                                    completion:^(NSError * _Nullable error) {
            // ...
        }];
    }];
}];

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

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

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

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

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

    Swift

    Auth.auth().signIn(
      withEmail: email,
      password: password
    ) { (result, error) in
      let authError = error as NSError
      if authError?.code == AuthErrorCode.secondFactorRequired.rawValue {
        // The user is a multi-factor user. Second factor challenge is required.
        let resolver =
          authError!.userInfo[AuthErrorUserInfoMultiFactorResolverKey] as! MultiFactorResolver
        // ...
      } else {
        // Handle other errors such as wrong password.
      }
    }
    

    Objective-C

    [FIRAuth.auth signInWithEmail:email
                         password:password
                       completion:^(FIRAuthDataResult * _Nullable authResult,
                                    NSError * _Nullable error) {
        if (error == nil || error.code != FIRAuthErrorCodeSecondFactorRequired) {
            // User is not enrolled with a second factor and is successfully signed in.
            // ...
        } else {
            // The user is a multi-factor user. Second factor challenge is required.
        }
    }];
    

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

  2. 如果用户注册了多个第二重身份验证,请询问用户要使用哪一个。您可以通过 resolver.hints[selectedIndex].phoneNumber 获取被部分遮盖的电话号码,使用 resolver.hints[selectedIndex].displayName 获取显示名。

    Swift

    // Ask user which second factor to use. Then:
    if resolver.hints[selectedIndex].factorID == PhoneMultiFactorID {
      // User selected a phone second factor.
      // ...
    } else if resolver.hints[selectedIndex].factorID == TotpMultiFactorID {
      // User selected a TOTP second factor.
      // ...
    } else {
      // Unsupported second factor.
    }
    

    Objective-C

    FIRMultiFactorResolver *resolver =
        (FIRMultiFactorResolver *) error.userInfo[FIRAuthErrorUserInfoMultiFactorResolverKey];
    
    // Ask user which second factor to use. Then:
    FIRPhoneMultiFactorInfo *hint = (FIRPhoneMultiFactorInfo *) resolver.hints[selectedIndex];
    if (hint.factorID == FIRPhoneMultiFactorID) {
      // User selected a phone second factor.
      // ...
    } else if (hint.factorID == FIRTOTPMultiFactorID) {
      // User selected a TOTP second factor.
      // ...
    } else {
      // Unsupported second factor.
    }
    
  3. 向用户的电话发送验证消息:

    Swift

    // Send SMS verification code.
    let hint = resolver.hints[selectedIndex] as! PhoneMultiFactorInfo
    PhoneAuthProvider.provider().verifyPhoneNumber(
      with: hint,
      uiDelegate: nil,
      multiFactorSession: resolver.session
    ) { (verificationId, error) in
      // verificationId will be needed for sign-in completion.
    }
    

    Objective-C

    // Send SMS verification code
    [FIRPhoneAuthProvider.provider
      verifyPhoneNumberWithMultiFactorInfo:hint
      UIDelegate:nil
      multiFactorSession:resolver.session
      completion:^(NSString * _Nullable verificationID, NSError * _Nullable error) {
        if (error != nil) {
            // Failed to verify phone number.
        }
    }];
    
  4. 短信验证码发出后,要求用户验证该验证码并用其来构建 PhoneAuthCredential

    Swift

    // Ask user for the verification code. Then:
    let credential = PhoneAuthProvider.provider().credential(
      withVerificationID: verificationId!,
      verificationCode: verificationCodeFromUser)
    

    Objective-C

    // Ask user for the SMS verification code. Then:
    FIRPhoneAuthCredential *credential =
        [FIRPhoneAuthProvider.provider
          credentialWithVerificationID:verificationID
                      verificationCode:verificationCodeFromUser];
    
  5. 使用凭据初始化断言对象:

    Swift

    let assertion = PhoneMultiFactorGenerator.assertion(with: credential)
    

    Objective-C

    FIRMultiFactorAssertion *assertion =
        [FIRPhoneMultiFactorGenerator assertionWithCredential:credential];
    
  6. 完成登录。然后,您可以访问原始登录结果,其中包括特定于提供商的标准数据和身份验证凭据:

    Swift

    // Complete sign-in. This will also trigger the Auth state listeners.
    resolver.resolveSignIn(with: assertion) { (authResult, error) in
      // authResult 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,
      // authResult.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.
    }
    

    Objective-C

    // Complete sign-in.
    [resolver resolveSignInWithAssertion:assertion
                              completion:^(FIRAuthDataResult * _Nullable authResult,
                                            NSError * _Nullable error) {
        if (error != nil) {
            // User successfully signed in with the second factor phone number.
        }
    }];
    

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

Swift

Auth.auth().signIn(
  withEmail: email,
  password: password
) { (result, error) in
  let authError = error as NSError?
  if authError?.code == AuthErrorCode.secondFactorRequired.rawValue {
    let resolver =
      authError!.userInfo[AuthErrorUserInfoMultiFactorResolverKey] as! MultiFactorResolver

    // Ask user which second factor to use.
    // ...

    // Then:
    let hint = resolver.hints[selectedIndex] as! PhoneMultiFactorInfo

    // Send SMS verification code
    PhoneAuthProvider.provider().verifyPhoneNumber(
      with: hint,
      uiDelegate: nil,
      multiFactorSession: resolver.session
    ) { (verificationId, error) in
      if error != nil {
        // Failed to verify phone number.
      }
      // Ask user for the SMS verification code.
      // ...

      // Then:
      let credential = PhoneAuthProvider.provider().credential(
        withVerificationID: verificationId!,
        verificationCode: verificationCodeFromUser)
      let assertion = PhoneMultiFactorGenerator.assertion(with: credential)

      // Complete sign-in.
      resolver.resolveSignIn(with: assertion) { (authResult, error) in
        if error != nil {
          // User successfully signed in with the second factor phone number.
        }
      }
    }
  }
}

Objective-C

[FIRAuth.auth signInWithEmail:email
                     password:password
                   completion:^(FIRAuthDataResult * _Nullable authResult,
                               NSError * _Nullable error) {
    if (error == nil || error.code != FIRAuthErrorCodeSecondFactorRequired) {
        // User is not enrolled with a second factor and is successfully signed in.
        // ...
    } else {
        FIRMultiFactorResolver *resolver =
            (FIRMultiFactorResolver *) error.userInfo[FIRAuthErrorUserInfoMultiFactorResolverKey];

        // Ask user which second factor to use.
        // ...

        // Then:
        FIRPhoneMultiFactorInfo *hint = (FIRPhoneMultiFactorInfo *) resolver.hints[selectedIndex];

        // Send SMS verification code
        [FIRPhoneAuthProvider.provider
          verifyPhoneNumberWithMultiFactorInfo:hint
                                    UIDelegate:nil
                            multiFactorSession:resolver.session
                                    completion:^(NSString * _Nullable verificationID,
                                                NSError * _Nullable error) {
            if (error != nil) {
                // Failed to verify phone number.
            }

            // Ask user for the SMS verification code.
            // ...

            // Then:
            FIRPhoneAuthCredential *credential =
                [FIRPhoneAuthProvider.provider
                  credentialWithVerificationID:verificationID
                              verificationCode:kPhoneSecondFactorVerificationCode];
            FIRMultiFactorAssertion *assertion =
                [FIRPhoneMultiFactorGenerator assertionWithCredential:credential];

            // Complete sign-in.
            [resolver resolveSignInWithAssertion:assertion
                                      completion:^(FIRAuthDataResult * _Nullable authResult,
                                                    NSError * _Nullable error) {
                if (error != nil) {
                    // User successfully signed in with the second factor phone number.
                }
            }];
        }];
    }
}];

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

后续步骤