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

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

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

准备工作

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

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

启用多重身份验证

  1. 转到 Cloud Console 中的 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. 选择云消息传递标签页。

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

    5. 选择密钥。

    6. 添加该密钥的密钥 ID。您可以在 Apple Developer Member Center 中的 Certificates, Identifiers & Profiles 下找到密钥 ID。

    7. 点击上传

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

使用 reCAPTCHA 验证

要让 Client SDK 能够使用 reCAPTCHA,请执行以下操作:

  1. 打开您的项目配置。

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

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

  4. 选择信息标签页。

  5. 展开网址类型部分。

  6. 点击 + 按钮。

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

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

自定义架构

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

选择注册模式

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

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

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

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

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

注册第二重身份验证

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

  1. 重新验证用户身份。

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

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

    Swift

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

    Objective-C

    [user.multiFactor getSessionWithCompletion:^(FIRMultiFactorSession * _Nullable session, NSError * _Nullable error) {
      // ...
    }];
    
  4. 向用户的手机发送验证消息:

    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. 短信验证码发出后,要求用户验证该验证码:

    Swift

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

    Objective-C

    // Ask user for the SMS verification code.
    FIRAuthCredential *credential = [FIRPhoneAuthProvider.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.
    [user.multiFactor enrollWithAssertion:assertion
      displayName:displayName
      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: kPhoneSecondFactorVerificationCode)
          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.
      FIRAuthCredential *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. 如果用户注册了多个第二重身份验证,询问他们要使用哪一个:

    Swift

    // 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 == FIRPhoneMultiFactorID) {
      // User selected a phone second factor.
      // ...
    } else {
      // Unsupported second factor.
      // Note that only phone second factors are currently supported.
    }
    

    Objective-C

    FIRMultiFactorResolver *resolver = (FIRMultiFactorResolver *)error.userInfo[FIRAuthErrorUserInfoMultiFactorResolverKey];
    // Ask user which second factor to use.
    FIRPhoneMultiFactorInfo *hint = resolver.hints[selectedIndex];
    
  3. 向用户的手机发送验证消息:

    Swift

    // Send SMS verification code.
    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. 短信验证码发出后,要求用户验证该验证码:

    Swift

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

    Objective-C

    // Ask user for the SMS verification code.
    FIRAuthCredential *credential = [FIRPhoneAuthProvider.provider credentialWithVerificationID:verificationID                                                                               verificationCode:kPhoneSecondFactorVerificationCode];
    
  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 == nil || authError!.code != AuthErrorCode.secondFactorRequired.rawValue) {
    // User is not enrolled with a second factor and is successfully signed in.
    // ...
  } else {
    let resolver = authError!.userInfo[AuthErrorUserInfoMultiFactorResolverKey] as! MultiFactorResolver
    // Ask user which second factor to use.
    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.
        let credential = PhoneAuthProvider.provider().credential(
          withVerificationID: verificationId!,
          verificationCode: kPhoneSecondFactorVerificationCode)
        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.
    FIRPhoneMultiFactorInfo *hint = 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.
        FIRAuthCredential *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.
          }
      }];
    }];
  }
}];

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

后续步骤