电话号码
使用 Firebase Authentication,您可以向用户的手机发送短信以协助其登录,用户使用短信中包含的一次性验证码即可登录。
如需将电话号码登录添加到您的应用,最简单的方法是使用 FirebaseUI,其中包含一个普适性登录 widget,可为电话号码登录、基于密码的登录和联合登录实现登录流程。本文档介绍了如何使用 Firebase SDK 实现电话号码登录流程。
须知事项
- 如果您尚未将您的应用与 Firebase 项目关联,请在 Firebase 控制台中进行关联。
-
使用 Swift Package Manager 安装和管理 Firebase 依赖项。
-
在 Xcode 中打开您的应用项目,依次点击 File(文件)> Add Packages(添加软件包)。
- 出现提示时,添加 Firebase Apple 平台 SDK 代码库:
https://github.com/firebase/firebase-ios-sdk.git
- 选择 Firebase Authentication 库。
- 将
-ObjC标志添加到目标 build 设置的“其他链接器标志”部分。 - 完成之后,Xcode 将会自动开始在后台解析和下载您的依赖项。
安全考量
与其他可用的方法相比,仅使用电话号码进行身份验证的方法虽然便捷,但安全性较低,因为电话号码的所有权可以很容易地在用户之间转移。此外,在具有多份用户个人资料的设备上,任何一位可以接收短信的用户都能使用该设备的电话号码登录账号。
如果您选择在应用中使用电话号码登录方法,应同时提供更安全的登录方法,并将使用电话号码登录的安全隐患告知用户。
为 Firebase 项目启用电话号码登录方法
如需让用户能够通过短信登录,您必须先为 Firebase 项目启用电话号码登录方法,步骤如下:
- 在 Firebase 控制台中,打开 Authentication 部分。
- 在登录方法页面上,启用电话号码登录方法。
- 在设置页面上,针对您要允许或禁止发送短信的区域设置一项政策。对于新项目,默认政策不允许任何区域。
启用应用验证
如需使用电话号码身份验证,Firebase 必须能够验证电话号码登录请求是否来自您的应用。Firebase Authentication 可通过以下两种方法完成此操作:
- 静默 APN 通知:用户首次通过电话号码在设备上登录时,Firebase Authentication 会使用静默推送通知向设备发送令牌。如果您的应用成功收到 Firebase 的通知,就可以继续使用电话号码进行登录。
对于 iOS 8.0 及更高版本,静默通知不需要用户明确同意,因此,即使用户拒绝在应用中接收 APNs 通知也不受影响。如此一来,在实现 Firebase 电话号码身份验证时,应用不需要请求用户授予权限即可接收推送通知。 - reCAPTCHA 验证:如果无法发送或接收静默推送通知(例如,如果用户为应用停用了后台刷新,或者当您在 iOS 模拟器上测试应用时),Firebase Authentication 会使用 reCAPTCHA 验证来完成电话登录流程。reCAPTCHA 验证任务通常可以在不需要用户回答任何问题的情况下完成。
如果正确配置了静默推送通知,只有极少数的用户会经历 reCAPTCHA 流程。尽管如此,您应该确保无论静默推送通知是否可用,电话号码登录功能都可正常工作。
开始接收静默通知
如需启用 APNs 通知以用于 Firebase Authentication,请执行以下操作:
- 在 Xcode 中为您的项目启用推送通知。
-
将您的 APNs 身份验证密钥上传到 Firebase。 如果您还没有 APNs 身份验证密钥,请务必在 Apple Developer Member Center 内创建一个。
-
在 Firebase 控制台中,在您的项目内依次选择齿轮图标 > 项目设置 > Cloud Messaging 标签页。
- 在 iOS 应用配置下的 APNs 身份验证密钥中,点击上传按钮,上传您的开发身份验证密钥、生产身份验证密钥,或两者。二者至少需要选择其一。
- 转到您保存密钥的位置,选择该密钥,然后点击打开。添加该密钥的 ID(可在 Apple Developer Member Center 中找到),然后点击上传。
如果您已有 APNs 证书,则可以改为上传证书。 3. 在 Xcode 中,为您的项目启用后台模式功能,然后选中后台提取和远程通知模式对应的复选框。
设置 reCAPTCHA 验证
如需让 Firebase SDK 能使用 reCAPTCHA 验证,请执行以下操作:
- 向您的 Xcode 项目添加自定义网址方案 (URL scheme):
- 打开您的项目配置:在左侧的树状视图中双击项目名称。在目标部分中选择您的应用,然后选择信息标签页,并展开网址类型部分。
-
点击 + 按钮,然后将经过编码的应用 ID 添加为网址方案。您可以打开 Firebase 控制台的常规设置页面,在您的 iOS 应用部分找到经过编码的应用 ID。请将其他字段留空。
完成上述操作后,您的配置应显示如下(但其中的值应替换为您的应用的值):
2. 可选:如果您希望自定义应用在向用户显示 reCAPTCHA 时如何呈现 SFSafariViewController,请创建一个符合AuthUIDelegate协议的自定义类,并将其传递给verifyPhoneNumber(_:uiDelegate:completion:)。
向用户的电话发送验证码
如需启动电话号码登录,请向用户显示一个提示其输入电话号码的界面,然后调用 verifyPhoneNumber(_:uiDelegate:completion:) 以请求 Firebase 通过短信向用户电话发送身份验证码:
- 获取用户的电话号码。
虽然相关的法律要求可能不尽相同,但为了避免用户不满,最佳做法是告知用户,如果他们选择使用电话号码登录方式,则可能会收到一条验证短信,并需按标准费率支付短信费用。
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:,以便让用户登录。
- 从用户处获取验证码。
- 根据验证码和验证 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 模拟器中进行开发。
- 可编写集成测试,而不会被通常在生产环境中应用于真实电话号码的安全检查所屏蔽。
虚构的电话号码必须满足以下要求:
- 确保使用确属虚构且不存在的电话号码。Firebase Authentication 不允许您将真实用户使用的现有电话号码设为测试号码。您可以使用以 555 为前缀的数字作为美国测试电话号码,例如 +1 650-555-3434
- 电话号码必须采用正确的格式,以符合长度要求和其他限制。这些电话号码仍将与真实用户的电话号码一样经过相同的验证。
- 您最多可以添加 10 个电话号码用于开发。
- 使用难以猜到的测试电话号码/验证码,并经常更换。
创建虚构的电话号码和验证码
- 在 Firebase 控制台中,打开 Authentication 部分。
- 在 Sign in method(登录方法)标签页中,启用电话号码提供方(如果您尚未启用)。
- 打开用于测试的电话号码折叠菜单。
- 提供您想要用于测试的电话号码,例如 +1 650-555-3434。
- 为该特定号码提供 6 位验证码,例如 654321。
- 添加该号码。如有需要,您可以删除电话号码及其验证码,只需将鼠标悬停在相应的行上并点击垃圾桶图标即可。
