您可以使用 Firebase 身份驗證,通過向用戶的手機發送短信來登錄用戶。用戶使用 SMS 消息中包含的一次性代碼登錄。
將電話號碼登錄添加到應用程序的最簡單方法是使用FirebaseUI ,其中包括一個下拉登錄小部件,該小部件可實現電話號碼登錄的登錄流程以及基於密碼的聯合登錄-在。本文檔介紹如何使用 Firebase SDK 實現電話號碼登錄流程。
在你開始之前
使用 Swift Package Manager 安裝和管理 Firebase 依賴項。
- 在 Xcode 中,打開應用程序項目,導航至File > Add Packages 。
- 出現提示時,添加 Firebase Apple 平台 SDK 存儲庫:
- 選擇 Firebase 身份驗證庫。
- 完成後,Xcode 將自動開始在後台解析並下載您的依賴項。
https://github.com/firebase/firebase-ios-sdk
- 如果您尚未將應用連接到 Firebase 項目,請從Firebase 控制台執行此操作。
安全問題
僅使用電話號碼進行身份驗證雖然方便,但安全性低於其他可用方法,因為電話號碼的所有權可以在用戶之間輕鬆轉移。此外,在具有多個用戶配置文件的設備上,任何可以接收 SMS 消息的用戶都可以使用設備的電話號碼登錄帳戶。
如果您在應用中使用基於電話號碼的登錄方式,則應將其與更安全的登錄方法一起提供,並告知用戶使用電話號碼登錄的安全權衡。
為您的 Firebase 項目啟用電話號碼登錄
要通過短信登錄用戶,您必須首先為您的 Firebase 項目啟用電話號碼登錄方法:
- 在Firebase 控制台中,打開“身份驗證”部分。
- 在登錄方法頁面上,啟用電話號碼登錄方法。
Firebase 的電話號碼登錄請求配額足夠高,大多數應用不會受到影響。但是,如果您需要通過電話身份驗證登錄大量用戶,則可能需要升級定價計劃。請參閱定價頁面。
啟用應用程序驗證
要使用電話號碼身份驗證,Firebase 必須能夠驗證電話號碼登錄請求是否來自您的應用。 Firebase 身份驗證有兩種方法可以實現此目的:
- 無聲 APN 通知:當您首次在設備上使用用戶的電話號碼登錄用戶時,Firebase 身份驗證會使用無聲推送通知向設備發送令牌。如果您的應用成功收到來自 Firebase 的通知,則可以繼續使用電話號碼登錄。
對於 iOS 8.0 及更高版本,靜默通知不需要用戶明確同意,因此不會受到用戶拒絕在應用程序中接收 APNs 通知的影響。因此,應用程序在實施 Firebase 電話號碼身份驗證時不需要請求用戶權限來接收推送通知。
- reCAPTCHA 驗證:如果無法發送或接收靜默推送通知,例如當用戶為您的應用禁用後台刷新時,或者在 iOS 模擬器上測試您的應用時,Firebase 身份驗證將使用 reCAPTCHA 驗證來完成電話驗證登錄流程。 reCAPTCHA 挑戰通常可以在用戶無需解決任何問題的情況下完成。
正確配置靜默推送通知後,只有極少數用戶會體驗 reCAPTCHA 流程。儘管如此,無論靜默推送通知是否可用,您都應該確保電話號碼登錄功能正常。
開始接收無聲通知
要啟用 APNs 通知以與 Firebase 身份驗證結合使用:
- 在 Xcode 中,為您的項目啟用推送通知。
將您的 APNs 身份驗證密鑰上傳到 Firebase。如果您還沒有 APNs 身份驗證密鑰,請確保在Apple 開發者會員中心創建一個。
在 Firebase 控制台的項目內,選擇齒輪圖標,選擇項目設置,然後選擇雲消息傳遞選項卡。
在iOS 應用程序配置下的APNs 身份驗證密鑰中,單擊上傳按鈕。
瀏覽到保存密鑰的位置,選擇它,然後單擊“打開” 。添加密鑰的密鑰 ID(可在Apple 開發者會員中心獲取)並單擊Upload 。
如果您已有 APNs 證書,則可以上傳該證書。
設置 reCAPTCHA 驗證
要使 Firebase SDK 能夠使用 reCAPTCHA 驗證,請執行以下操作:
- 將自定義 URL 方案添加到您的 Xcode 項目:
- 打開項目配置:雙擊左側樹視圖中的項目名稱。從“目標”部分選擇您的應用程序,然後選擇“信息”選項卡,並展開“URL 類型”部分。
- 單擊+按鈕,並將您的編碼應用程序 ID 添加為 URL 方案。您可以在 Firebase 控制台的“常規設置”頁面上的 iOS 應用部分中找到您的編碼應用 ID。將其他字段留空。
完成後,您的配置應類似於以下內容(但具有特定於應用程序的值):
- 可選:如果要自定義應用程序在向用戶顯示 reCAPTCHA 時呈現
SFSafariViewController
的方式,請創建一個符合AuthUIDelegate
協議的自定義類,並將其傳遞給verifyPhoneNumber(_:uiDelegate:completion:)
。
發送驗證碼至用戶手機
要啟動電話號碼登錄,請向用戶呈現一個界面,提示他們提供電話號碼,然後調用verifyPhoneNumber(_:uiDelegate:completion:)
請求 Firebase 通過短信向用戶的手機發送身份驗證代碼:
獲取用戶的電話號碼。
法律要求各不相同,但作為最佳實踐並為用戶設定期望,您應該告知他們,如果他們使用電話登錄,他們可能會收到一條用於驗證的短信,並且適用標準費率。
- 調用
verifyPhoneNumber(_:uiDelegate:completion:)
,將用戶的電話號碼傳遞給它。迅速
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 來登錄用戶。Firebase 發送的 SMS 消息還可以通過 Auth 實例上的
languageCode
屬性指定身份驗證語言來本地化。迅速
// Change language code to french. Auth.auth().languageCode = "fr";
Objective-C
// Change language code to french. [FIRAuth auth].languageCode = @"fr";
保存驗證 ID 並在應用加載時恢復它。通過這樣做,如果您的應用程序在用戶完成登錄流程之前終止(例如,切換到短信應用程序時),您可以確保您仍然擁有有效的驗證 ID。
您可以以任何您想要的方式保留驗證 ID。一個簡單的方法是使用
NSUserDefaults
對象保存驗證 ID:迅速
UserDefaults.standard.set(verificationID, forKey: "authVerificationID")
Objective-C
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; [defaults setObject:verificationID forKey:@"authVerificationID"];
然後,您可以恢復保存的值:
迅速
let verificationID = UserDefaults.standard.string(forKey: "authVerificationID")
Objective-C
NSString *verificationID = [defaults stringForKey:@"authVerificationID"];
如果對verifyPhoneNumber(_:uiDelegate:completion:)
調用成功,您可以在用戶收到短信中的驗證碼時提示用戶輸入驗證碼。
使用驗證碼登錄用戶
用戶向您的應用程序提供短信中的驗證碼後,通過根據驗證碼和驗證 ID 創建FIRPhoneAuthCredential
對象並將該對像傳遞給signInWithCredential:completion:
來登錄用戶。
- 獲取用戶的驗證碼。
- 根據驗證碼和驗證 ID 創建
FIRPhoneAuthCredential
對象。迅速
let credential = PhoneAuthProvider.provider().credential( withVerificationID: verificationID, verificationCode: verificationCode )
Objective-C
FIRAuthCredential *credential = [[FIRPhoneAuthProvider provider] credentialWithVerificationID:verificationID verificationCode:userInput];
- 使用
FIRPhoneAuthCredential
對象登錄用戶:迅速
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 控制台設置用於開發的虛構電話號碼。使用虛構電話號碼進行測試具有以下優點:
- 測試電話號碼身份驗證而不消耗您的使用配額。
- 在不發送實際短信的情況下測試電話號碼身份驗證。
- 使用相同的電話號碼運行連續測試而不會受到限制。如果審核者碰巧使用相同的電話號碼進行測試,這可以最大限度地降低應用商店審核過程中被拒絕的風險。
- 無需任何額外工作即可在開發環境中輕鬆進行測試,例如無需 Google Play 服務即可在 iOS 模擬器或 Android 模擬器中進行開發。
- 編寫集成測試,而不會被通常應用於生產環境中真實電話號碼的安全檢查阻止。
虛構電話號碼必須滿足以下要求:
- 確保您使用的電話號碼確實是虛構的且不存在。 Firebase 身份驗證不允許您將真實用戶使用的現有電話號碼設置為測試號碼。一種選擇是使用 555 前綴號碼作為美國測試電話號碼,例如: +1 650-555-3434
- 電話號碼的格式必須正確,以符合長度和其他限制。它們仍將經過與真實用戶電話號碼相同的驗證。
- 您最多可以添加 10 個電話號碼進行開發。
- 使用難以猜測的測試電話號碼/代碼並經常更改。
創建虛構的電話號碼和驗證碼
- 在Firebase 控制台中,打開“身份驗證”部分。
- 在登錄方法選項卡中,啟用電話提供商(如果尚未啟用)。
- 打開用於測試手風琴菜單的電話號碼。
- 提供您要測試的電話號碼,例如: +1 650-555-3434 。
- 提供該特定號碼的 6 位驗證碼,例如: 654321 。
- 添加號碼。如果需要,您可以將鼠標懸停在相應行上並單擊垃圾桶圖標來刪除電話號碼及其代碼。
手動測試
您可以直接在應用程序中開始使用虛構的電話號碼。這使您可以在開發階段執行手動測試,而不會遇到配額問題或限制。您還可以直接從 iOS 模擬器或 Android 模擬器進行測試,而無需安裝 Google Play 服務。
當您提供虛構的電話號碼並發送驗證碼時,不會發送實際的短信。相反,您需要提供之前配置的驗證碼才能完成登錄。
登錄完成後,系統會使用該電話號碼創建 Firebase 用戶。用戶具有與真實電話號碼用戶相同的行為和屬性,並且可以以相同的方式訪問實時數據庫/Cloud Firestore 和其他服務。在此過程中生成的 ID 令牌與真實電話號碼用戶具有相同的簽名。
如果您想進一步限制訪問,另一種選擇是通過自定義聲明對這些用戶設置測試角色,以將他們區分為假用戶。
集成測試
除了手動測試之外,Firebase 身份驗證還提供 API 來幫助編寫電話身份驗證測試的集成測試。這些 API 通過禁用 Web 中的 reCAPTCHA 要求和 iOS 中的靜默推送通知來禁用應用程序驗證。這使得這些流程中的自動化測試成為可能並且更容易實施。此外,它們還有助於提供在 Android 上測試即時驗證流程的能力。
在 iOS 上,在調用verifyPhoneNumber
之前,必須將appVerificationDisabledForTesting
設置設置為TRUE
。處理過程不需要任何 APNs 令牌或在後台發送靜默推送通知,從而更容易在模擬器中進行測試。這也會禁用 reCAPTCHA 後備流程。
請注意,禁用應用驗證後,使用非虛構電話號碼將無法完成登錄。此 API 只能使用虛構電話號碼。
迅速
let phoneNumber = "+16505554567" // This test verification code is specified for the given test phone number in the developer console. let testVerificationCode = "123456" Auth.auth().settings.isAppVerificationDisabledForTesting = TRUE PhoneAuthProvider.provider().verifyPhoneNumber(phoneNumber, uiDelegate:nil) { verificationID, error in if (error) { // Handles error self.handleError(error) return } let credential = PhoneAuthProvider.provider().credential(withVerificationID: verificationID ?? "", verificationCode: testVerificationCode) Auth.auth().signInAndRetrieveData(with: credential) { authData, error in if (error) { // Handles error self.handleError(error) return } _user = authData.user }]; }];
Objective-C
NSString *phoneNumber = @"+16505554567"; // This test verification code is specified for the given test phone number in the developer console. NSString *testVerificationCode = @"123456"; [FIRAuth auth].settings.appVerificationDisabledForTesting = YES; [[FIRPhoneAuthProvider provider] verifyPhoneNumber:phoneNumber completion:^(NSString *_Nullable verificationID, NSError *_Nullable error) { if (error) { // Handles error [self handleError:error]; return; } FIRAuthCredential *credential = [FIRPhoneAuthProvider credentialWithVerificationID:verificationID verificationCode:testVerificationCode]; [FIRAuth auth] signInWithAndRetrieveDataWithCredential:credential completion:^(FIRUser *_Nullable user, NSError *_Nullable error) { if (error) { // Handles error [self handleError:error]; return; } _user = user; }]; }];
附錄:使用手機登錄而不使用 swizzling
Firebase 身份驗證使用方法調配來自動獲取應用的 APNs 令牌,處理 Firebase 發送到您的應用的靜默推送通知,並在驗證期間自動攔截來自 reCAPTCHA 驗證頁面的自定義方案重定向。
如果您不想使用 swizzling,可以通過將FirebaseAppDelegateProxyEnabled
標誌添加到應用程序的 Info.plist 文件並將其設置為NO
來禁用它。請注意,將此標誌設置為NO
還會禁用其他 Firebase 產品(包括 Firebase Cloud Messaging)的 swizzling。
如果禁用混合,則必須將 APNs 設備令牌、推送通知和自定義方案重定向 URL 顯式傳遞到 Firebase 身份驗證。
如果您正在構建 SwiftUI 應用程序,您還應該顯式地將 APNs 設備令牌、推送通知和自定義方案重定向 URL 傳遞到 Firebase 身份驗證。
要獲取 APNs 設備令牌,請實現application(_:didRegisterForRemoteNotificationsWithDeviceToken:)
方法,並將設備令牌傳遞給Auth
的setAPNSToken(_:type:)
方法。
迅速
func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) { // Pass device token to auth Auth.auth().setAPNSToken(deviceToken, type: .prod) // Further handling of the device token if needed by the app // ... }
Objective-C
- (void)application:(UIApplication *)application didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken { // Pass device token to auth. [[FIRAuth auth] setAPNSToken:deviceToken type:FIRAuthAPNSTokenTypeProd]; // Further handling of the device token if needed by the app. }
要處理推送通知,請在application(_:didReceiveRemoteNotification:fetchCompletionHandler:):
方法中,通過調用Auth
的canHandleNotification(_:)
方法來檢查與 Firebase 身份驗證相關的通知。
迅速
func application(_ application: UIApplication, didReceiveRemoteNotification notification: [AnyHashable : Any], fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) { if Auth.auth().canHandleNotification(notification) { completionHandler(.noData) return } // This notification is not auth related; it should be handled separately. }
Objective-C
- (void)application:(UIApplication *)application didReceiveRemoteNotification:(NSDictionary *)notification fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler { // Pass notification to auth and check if they can handle it. if ([[FIRAuth auth] canHandleNotification:notification]) { completionHandler(UIBackgroundFetchResultNoData); return; } // This notification is not auth related; it should be handled separately. }
要處理自定義方案重定向 URL,請實現application(_:open:options:)
方法,並在其中將 URL 傳遞給Auth
的canHandleURL(_:)
方法。
迅速
func application(_ application: UIApplication, open url: URL, options: [UIApplicationOpenURLOptionsKey : Any]) -> Bool { if Auth.auth().canHandle(url) { return true } // URL not auth related; it should be handled separately. }
Objective-C
- (BOOL)application:(UIApplication *)app openURL:(NSURL *)url options:(NSDictionary<UIApplicationOpenURLOptionsKey, id> *)options { if ([[FIRAuth auth] canHandleURL:url]) { return YES; } // URL not auth related; it should be handled separately. }
如果您使用 SwiftUI 或UISceneDelegate
來處理重定向 URL,請實現scene(_:openURLContexts:)
方法,並在其中將 URL 傳遞給Auth
的canHandleURL(_:)
方法。
迅速
func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>) { for urlContext in URLContexts { let url = urlContext.url Auth.auth().canHandle(url) } // URL not auth related; it should be handled separately. }
Objective-C
- (void)scene:(UIScene *)scene openURLContexts:(NSSet<UIOpenURLContext *> *)URLContexts { for (UIOpenURLContext *urlContext in URLContexts) { [FIRAuth.auth canHandleURL:urlContext.url]; // URL not auth related; it should be handled separately. } }
下一步
用戶首次登錄後,系統會創建一個新的用戶帳戶,並將其鏈接到用戶登錄時使用的憑據(即用戶名和密碼、電話號碼或身份驗證提供商信息)。此新帳戶將作為 Firebase 項目的一部分存儲,並且可用於識別項目中每個應用中的用戶,無論用戶如何登錄。
在 Firebase 實時數據庫和雲存儲安全規則中,您可以從
auth
變量獲取登錄用戶的唯一用戶 ID,並使用它來控制用戶可以訪問哪些數據。
您可以通過將身份驗證提供程序憑據鏈接到現有用戶帳戶,允許用戶使用多個身份驗證提供程序登錄您的應用程序。
要註銷用戶,請調用signOut:
迅速
let firebaseAuth = Auth.auth() do { try firebaseAuth.signOut() } catch let signOutError as NSError { print("Error signing out: %@", signOutError) }
Objective-C
NSError *signOutError; BOOL status = [[FIRAuth auth] signOut:&signOutError]; if (!status) { NSLog(@"Error signing out: %@", signOutError); return; }
您可能還需要為所有身份驗證錯誤添加錯誤處理代碼。請參閱處理錯誤。