电话号码

使用 Firebase Authentication,您可以向用户的手机发送短信以协助其登录,用户使用短信中包含的一次性验证码即可登录。

如需将电话号码登录添加到您的应用,最简单的方法是使用 FirebaseUI,其中包含一个普适性登录 widget,可为电话号码登录、基于密码的登录和联合登录实现登录流程。本文档介绍了如何使用 Firebase SDK 实现电话号码登录流程。

须知事项

  1. 如果您尚未将您的应用与 Firebase 项目关联,请在 Firebase 控制台中进行关联。
  2. 使用 Swift Package Manager 安装和管理 Firebase 依赖项。

  3. 在 Xcode 中打开您的应用项目,依次点击 File(文件)> Add Packages(添加软件包)。

  4. 出现提示时,添加 Firebase Apple 平台 SDK 代码库:

https://github.com/firebase/firebase-ios-sdk.git

  1. 选择 Firebase Authentication 库。
  2. 将 -ObjC 标志添加到目标 build 设置的“其他链接器标志”部分。
  3. 完成之后,Xcode 将会自动开始在后台解析和下载您的依赖项。

安全考量

与其他可用的方法相比,仅使用电话号码进行身份验证的方法虽然便捷,但安全性较低,因为电话号码的所有权可以很容易地在用户之间转移。此外,在具有多份用户个人资料的设备上,任何一位可以接收短信的用户都能使用该设备的电话号码登录账号。

如果您选择在应用中使用电话号码登录方法,应同时提供更安全的登录方法,并将使用电话号码登录的安全隐患告知用户。

为 Firebase 项目启用电话号码登录方法

如需让用户能够通过短信登录,您必须先为 Firebase 项目启用电话号码登录方法,步骤如下:

  1. 在 Firebase 控制台中,打开 Authentication 部分。
  2. 在登录方法页面上,启用电话号码登录方法。
  3. 在设置页面上,针对您要允许或禁止发送短信的区域设置一项政策。对于新项目,默认政策不允许任何区域。

启用应用验证

如需使用电话号码身份验证,Firebase 必须能够验证电话号码登录请求是否来自您的应用。Firebase Authentication 可通过以下两种方法完成此操作:

  • 静默 APN 通知:用户首次通过电话号码在设备上登录时,Firebase Authentication 会使用静默推送通知向设备发送令牌。如果您的应用成功收到 Firebase 的通知,就可以继续使用电话号码进行登录。

对于 iOS 8.0 及更高版本,静默通知不需要用户明确同意,因此,即使用户拒绝在应用中接收 APNs 通知也不受影响。如此一来,在实现 Firebase 电话号码身份验证时,应用不需要请求用户授予权限即可接收推送通知。 - reCAPTCHA 验证:如果无法发送或接收静默推送通知(例如,如果用户为应用停用了后台刷新,或者当您在 iOS 模拟器上测试应用时),Firebase Authentication 会使用 reCAPTCHA 验证来完成电话登录流程。reCAPTCHA 验证任务通常可以在不需要用户回答任何问题的情况下完成。

如果正确配置了静默推送通知,只有极少数的用户会经历 reCAPTCHA 流程。尽管如此,您应该确保无论静默推送通知是否可用,电话号码登录功能都可正常工作。

开始接收静默通知

如需启用 APNs 通知以用于 Firebase Authentication,请执行以下操作:

  1. 在 Xcode 中为您的项目启用推送通知。
  2. 将您的 APNs 身份验证密钥上传到 Firebase。 如果您还没有 APNs 身份验证密钥,请务必在 Apple Developer Member Center 内创建一个。

  3. 在 Firebase 控制台中,在您的项目内依次选择齿轮图标 > 项目设置 > Cloud Messaging 标签页。

  4. 在 iOS 应用配置下的 APNs 身份验证密钥中,点击上传按钮,上传您的开发身份验证密钥、生产身份验证密钥,或两者。二者至少需要选择其一。
  5. 转到您保存密钥的位置,选择该密钥,然后点击打开。添加该密钥的 ID(可在 Apple Developer Member Center 中找到),然后点击上传。

如果您已有 APNs 证书,则可以改为上传证书。 3. 在 Xcode 中,为您的项目启用后台模式功能,然后选中后台提取和远程通知模式对应的复选框。

设置 reCAPTCHA 验证

如需让 Firebase SDK 能使用 reCAPTCHA 验证,请执行以下操作:

  1. 向您的 Xcode 项目添加自定义网址方案 (URL scheme):
  2. 打开您的项目配置:在左侧的树状视图中双击项目名称。在目标部分中选择您的应用,然后选择信息标签页,并展开网址类型部分。
  3. 点击 + 按钮,然后将经过编码的应用 ID 添加为网址方案。您可以打开 Firebase 控制台的常规设置页面,在您的 iOS 应用部分找到经过编码的应用 ID。请将其他字段留空。

    完成上述操作后,您的配置应显示如下(但其中的值应替换为您的应用的值):

    Xcode 自定义网址方案设置界面的屏幕截图 2. 可选:如果您希望自定义应用在向用户显示 reCAPTCHA 时如何呈现 SFSafariViewController,请创建一个符合 AuthUIDelegate 协议的自定义类,并将其传递给 verifyPhoneNumber(_:uiDelegate:completion:)。

向用户的电话发送验证码

如需启动电话号码登录,请向用户显示一个提示其输入电话号码的界面,然后调用 verifyPhoneNumber(_:uiDelegate:completion:) 以请求 Firebase 通过短信向用户电话发送身份验证码:

  1. 获取用户的电话号码。

虽然相关的法律要求可能不尽相同,但为了避免用户不满,最佳做法是告知用户,如果他们选择使用电话号码登录方式,则可能会收到一条验证短信,并需按标准费率支付短信费用。 2. 调用 verifyPhoneNumber(_:uiDelegate:completion:),并向其传递用户的电话号码。

### Swift

PhoneAuthProvider.provider() .verifyPhoneNumber(phoneNumber, uiDelegate: nil) { verificationID, error in if let error = error { self.showMessagePrompt(error.localizedDescription) return } // Sign in using the verificationID and the code sent to the user // ... }

### Objective-C

[[FIRPhoneAuthProvider provider] verifyPhoneNumber:userInput UIDelegate:nil completion:^(NSString * _Nullable verificationID, NSError * _Nullable error) { if (error) { [self showMessagePrompt:error.localizedDescription]; return; } // Sign in using the verificationID and the code sent to the user // ... }];

verifyPhoneNumber 方法是可重入的:如果您对其进行多次调用(例如在某个视图的 onAppear 方法中),除非原始请求已超时,否则 verifyPhoneNumber 方法将不会发送第二条短信。

当您调用 verifyPhoneNumber(_:uiDelegate:completion:) 时,Firebase 会向您的应用发送一条静默推送通知,或向用户发起 reCAPTCHA 验证任务。在您的应用收到通知或者用户完成 reCAPTCHA 验证任务后,Firebase 会向指定的电话号码发送一条包含身份验证码的短信,并将验证 ID 传递给您的完成函数。您需要有验证码和验证 ID 才能让用户登录。

您还可以通过 Auth 实例中的 languageCode 属性指定身份验证语言,以对 Firebase 发送的短信进行本地化。

#### Swift

// Change language code to french. Auth.auth().languageCode = "fr";

#### Objective-C

// Change language code to french. [FIRAuth auth].languageCode = @"fr"; 3. 保存验证 ID,并在应用加载时将其还原。这样可确保即使您的应用在用户完成登录流程之前被终止(例如,用户切换到短信应用时),您仍然保留有效的验证 ID。

您可以用任何方式留存验证 ID。一个简单的方法是使用 NSUserDefaults 对象保存验证 ID:

### Swift

UserDefaults.standard.set(verificationID, forKey: "authVerificationID")

### Objective-C

NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; [defaults setObject:verificationID forKey:@"authVerificationID"];

然后,您可以还原保存的值:

### Swift

let verificationID = UserDefaults.standard.string(forKey: "authVerificationID")

### Objective-C

NSString *verificationID = [defaults stringForKey:@"authVerificationID"];

如果成功调用 verifyPhoneNumber(_:uiDelegate:completion:),那么您可以在用户收到短信验证码时提示用户输入验证码。

使用验证码让用户登录

用户将短信中的验证码提供给您的应用之后,系统会根据验证码和验证 ID 创建一个 FIRPhoneAuthCredential 对象并将此对象传递给 signInWithCredential:completion:,以便让用户登录。

  1. 从用户处获取验证码。
  2. 根据验证码和验证 ID 创建 FIRPhoneAuthCredential 对象。

### Swift

let credential = PhoneAuthProvider.provider().credential( withVerificationID: verificationID, verificationCode: verificationCode )

### Objective-C

FIRAuthCredential *credential = [[FIRPhoneAuthProvider provider] credentialWithVerificationID:verificationID verificationCode:userInput]; 3. 使用 FIRPhoneAuthCredential 对象让用户登录:

### Swift

Auth.auth().signIn(with: credential) { authResult, error in if let error = error { let authError = error as NSError if isMFAEnabled, authError.code == AuthErrorCode.secondFactorRequired.rawValue { // The user is a multi-factor user. Second factor challenge is required. let resolver = authError .userInfo[AuthErrorUserInfoMultiFactorResolverKey] as! MultiFactorResolver var displayNameString = "" for tmpFactorInfo in resolver.hints { displayNameString += tmpFactorInfo.displayName ?? "" displayNameString += " " } self.showTextInputPrompt( withMessage: "Select factor to sign in\n\(displayNameString)", completionBlock: { userPressedOK, displayName in var selectedHint: PhoneMultiFactorInfo? for tmpFactorInfo in resolver.hints { if displayName == tmpFactorInfo.displayName { selectedHint = tmpFactorInfo as? PhoneMultiFactorInfo } } PhoneAuthProvider.provider() .verifyPhoneNumber(with: selectedHint!, uiDelegate: nil, multiFactorSession: resolver .session) { verificationID, error in if error != nil { print( "Multi factor start sign in failed. Error: \(error.debugDescription)" ) } else { self.showTextInputPrompt( withMessage: "Verification code for \(selectedHint?.displayName ?? "")", completionBlock: { userPressedOK, verificationCode in let credential: PhoneAuthCredential? = PhoneAuthProvider.provider() .credential(withVerificationID: verificationID!, verificationCode: verificationCode!) let assertion: MultiFactorAssertion? = PhoneMultiFactorGenerator .assertion(with: credential!) resolver.resolveSignIn(with: assertion!) { authResult, error in if error != nil { print( "Multi factor finanlize sign in failed. Error: \(error.debugDescription)" ) } else { self.navigationController?.popViewController(animated: true) } } } ) } } } ) } else { self.showMessagePrompt(error.localizedDescription) return } // ... return } // User is signed in // ... }

### Objective-C

[[FIRAuth auth] signInWithCredential:credential completion:^(FIRAuthDataResult * _Nullable authResult, NSError * _Nullable error) { if (isMFAEnabled && error && error.code == FIRAuthErrorCodeSecondFactorRequired) { FIRMultiFactorResolver *resolver = error.userInfo[FIRAuthErrorUserInfoMultiFactorResolverKey]; NSMutableString *displayNameString = [NSMutableString string]; for (FIRMultiFactorInfo *tmpFactorInfo in resolver.hints) { [displayNameString appendString:tmpFactorInfo.displayName]; [displayNameString appendString:@" "]; } [self showTextInputPromptWithMessage:[NSString stringWithFormat:@"Select factor to sign in\n%@", displayNameString] completionBlock:^(BOOL userPressedOK, NSString *_Nullable displayName) { FIRPhoneMultiFactorInfo* selectedHint; for (FIRMultiFactorInfo *tmpFactorInfo in resolver.hints) { if ([displayName isEqualToString:tmpFactorInfo.displayName]) { selectedHint = (FIRPhoneMultiFactorInfo *)tmpFactorInfo; } } [FIRPhoneAuthProvider.provider verifyPhoneNumberWithMultiFactorInfo:selectedHint UIDelegate:nil multiFactorSession:resolver.session completion:^(NSString * _Nullable verificationID, NSError * _Nullable error) { if (error) { [self showMessagePrompt:error.localizedDescription]; } else { [self showTextInputPromptWithMessage:[NSString stringWithFormat:@"Verification code for %@", selectedHint.displayName] completionBlock:^(BOOL userPressedOK, NSString *_Nullable verificationCode) { FIRPhoneAuthCredential *credential = [[FIRPhoneAuthProvider provider] credentialWithVerificationID:verificationID verificationCode:verificationCode]; FIRMultiFactorAssertion *assertion = [FIRPhoneMultiFactorGenerator assertionWithCredential:credential]; [resolver resolveSignInWithAssertion:assertion completion:^(FIRAuthDataResult * _Nullable authResult, NSError * _Nullable error) { if (error) { [self showMessagePrompt:error.localizedDescription]; } else { NSLog(@"Multi factor finanlize sign in succeeded."); } }]; }]; } }]; }]; } else if (error) { // ... return; } // User successfully signed in. Get user data from the FIRUser object if (authResult == nil) { return; } FIRUser *user = authResult.user; // ... }];

使用虚构的电话号码进行测试

您可以通过 Firebase 控制台设置虚构电话号码以用于开发。使用虚构的电话号码进行测试具有以下优势:

  • 测试电话号码身份验证时不会占用使用量配额。
  • 测试电话号码身份验证时无需发送实际的短信。
  • 可使用同一电话号码运行连续测试,而不会受到短信发送数量的限制。如果审核人员恰好使用相同的电话号码进行测试,这项优势会使在 App Store 审核过程中遭拒的风险降至最低。
  • 无需其他操作即可在开发环境中轻松进行测试,例如可以在 iOS 模拟器或没有 Google Play 服务的 Android 模拟器中进行开发。
  • 可编写集成测试,而不会被通常在生产环境中应用于真实电话号码的安全检查所屏蔽。

虚构的电话号码必须满足以下要求:

  1. 确保使用确属虚构且不存在的电话号码。Firebase Authentication 不允许您将真实用户使用的现有电话号码设为测试号码。您可以使用以 555 为前缀的数字作为美国测试电话号码,例如 +1 650-555-3434
  2. 电话号码必须采用正确的格式,以符合长度要求和其他限制。这些电话号码仍将与真实用户的电话号码一样经过相同的验证。
  3. 您最多可以添加 10 个电话号码用于开发。
  4. 使用难以猜到的测试电话号码/验证码,并经常更换。

创建虚构的电话号码和验证码

  1. 在 Firebase 控制台中,打开 Authentication 部分。
  2. 在 Sign in method(登录方法)标签页中,启用电话号码提供方(如果您尚未启用)。
  3. 打开用于测试的电话号码折叠菜单。
  4. 提供您想要用于测试的电话号码,例如 +1 650-555-3434。
  5. 为该特定号码提供 6 位验证码,例如 654321。
  6. 添加该号码。如有需要,您可以删除电话号码及其验证码,只需将鼠标悬停在相应的行上并点击垃圾桶图标即可。