您可以使用Firebase身份驗證通過將SMS消息發送到用戶的電話來登錄用戶。用戶使用SMS消息中包含的一次性代碼登錄。
將電話號碼登錄添加到您的應用程序最簡單的方法是使用FirebaseUI ,它包括一個插入式登錄小部件,該小部件可實現電話號碼登錄的登錄流程以及基於密碼的聯合登錄-在。本文檔介紹瞭如何使用Firebase SDK實施電話號碼登錄流程。
在你開始之前
- 將Firebase添加到您的iOS項目。
- 在您的
Podfile
包括以下Pod:pod 'Firebase/Auth'
- 如果您尚未將應用程序連接到Firebase項目,請從Firebase控制台進行。
安全問題
僅使用電話號碼進行身份驗證雖然方便,但比其他可用方法安全性低,因為擁有電話號碼的身份可以在用戶之間輕鬆轉移。同樣,在具有多個用戶配置文件的設備上,任何可以接收SMS消息的用戶都可以使用設備的電話號碼登錄到帳戶。
如果您在應用中使用基於電話號碼的登錄,則應將其與更安全的登錄方法一起提供,並告知用戶使用電話號碼登錄的安全權衡。
為您的Firebase項目啟用電話號碼登錄
要通過SMS登錄用戶,必須首先為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身份驗證密鑰,請參閱使用FCM配置APNs 。
在Firebase控制台的項目內部,選擇齒輪圖標,選擇Project Settings ,然後選擇Cloud Messaging選項卡。
在iOS應用配置下的APNs身份驗證密鑰中,點擊上傳按鈕。
瀏覽到保存密鑰的位置,選擇它,然後單擊“打開” 。添加密鑰的密鑰ID(可在Apple Developer Member Center中的證書,標識符和配置文件中找到),然後單擊Upload 。
如果您已經擁有APNs證書,則可以上傳該證書。
設置reCAPTCHA驗證
要使Firebase SDK能夠使用reCAPTCHA驗證,請執行以下操作:
- 將自定義URL方案添加到您的Xcode項目中:
- 打開項目配置:在左樹視圖中雙擊項目名稱。從“目標”部分中選擇您的應用,然後選擇“信息”選項卡,然後展開“ URL類型”部分。
- 單擊+按鈕,然後為您的反向客戶端ID添加URL方案。要找到此值,請打開
配置文件,然後查找GoogleService-Info.plist REVERSED_CLIENT_ID
密鑰。複製該鍵的值,然後將其粘貼到配置頁上的“ URL方案”框中。將其他字段留空。完成後,您的配置應類似於以下內容(但具有特定於應用程序的值):
- 可選:如果要自定義應用程序在向用戶顯示reCAPTCHA時呈現
SFSafariViewController
或UIWebView
的方式,請創建一個符合FIRAuthUIDelegate
協議的自定義類,並將其傳遞給verifyPhoneNumber:UIDelegate:completion:
將驗證碼發送到用戶的手機
要啟動電話號碼登錄,請向用戶顯示一個界面,提示他們提供他們的電話號碼,然後調用verifyPhoneNumber:UIDelegate:completion:
要求Firebase通過SMS將驗證碼發送到用戶的電話:
獲取用戶的電話號碼。
法律要求各不相同,但是作為最佳實踐並為用戶設定期望,您應該告知他們,如果他們使用電話登錄,則他們可能會收到SMS消息以進行驗證,並採用標準費率。
- 調用
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 // ... }
目標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:UIDelegate:completion:
,Firebase會向您的應用程序發送靜默推送通知,或向用戶發出reCAPTCHA挑戰。在您的應用收到通知或用戶完成reCAPTCHA挑戰後,Firebase將包含驗證碼的SMS消息發送到指定的電話號碼,並將驗證ID傳遞給您的完成功能。您將需要驗證碼和驗證ID來登錄用戶。也可以通過在Auth實例上的
languageCode
屬性指定身份驗證語言來本地化Firebase發送的SMS消息。迅速
// Change language code to french. Auth.auth().languageCode = "fr";
目標C
// Change language code to french. [FIRAuth auth].languageCode = @"fr";
保存驗證ID並在應用加載時將其還原。這樣,如果您的應用在用戶完成登錄流程之前(例如,切換到SMS應用時)終止,則可以確保您仍然具有有效的驗證ID。
您可以使用任何方式保留驗證ID。一種簡單的方法是使用
NSUserDefaults
對象保存驗證ID:迅速
UserDefaults.standard.set(verificationID, forKey: "authVerificationID")
目標C
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults]; [defaults setObject:verificationID forKey:@"authVerificationID"];
然後,您可以恢復保存的值:
迅速
let verificationID = UserDefaults.standard.string(forKey: "authVerificationID")
目標C
NSString *verificationID = [defaults stringForKey:@"authVerificationID"];
如果對verifyPhoneNumber:UIDelegate:completion:
的調用成功,則當用戶在SMS消息中收到驗證碼時,可以提示用戶鍵入驗證碼。
使用驗證碼登錄用戶
用戶向您的應用程序提供SMS消息中的驗證碼後,通過根據驗證碼和驗證ID創建FIRPhoneAuthCredential
對象並將該對signInWithCredential:completion:
傳遞給signInWithCredential:completion:
來signInWithCredential:completion:
。
- 從用戶那裡獲取驗證碼。
- 根據驗證碼和驗證ID創建
FIRPhoneAuthCredential
對象。迅速
let credential = PhoneAuthProvider.provider().credential( withVerificationID: verificationID, verificationCode: verificationCode)
物鏡
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 // ... }
物鏡
[[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控制台設置虛擬電話號碼進行開發。使用虛擬電話號碼進行測試可以帶來以下好處:
- 在不消耗您的使用配額的情況下測試電話號碼身份驗證。
- 測試電話號碼身份驗證,而不發送實際的SMS消息。
- 使用相同的電話號碼連續運行測試,而不會受到限制。如果審閱者碰巧使用相同的電話號碼進行測試,則可以最大程度地減少在App Store審閱過程中被拒絕的風險。
- 無需任何額外的努力即可在開發環境中輕鬆測試,例如可以在沒有Google Play服務的iOS模擬器或Android模擬器中進行開發。
- 編寫集成測試,而不會受到通常在生產環境中應用於真實電話號碼的安全檢查的阻止。
虛構電話號碼必須符合以下要求:
- 確保使用的電話號碼確實是虛構的,尚不存在。 Firebase身份驗證不允許您將真實用戶使用的現有電話號碼設置為測試號碼。一種選擇是使用555前綴號碼作為美國測試電話號碼,例如: +1 650-555-3434
- 電話號碼必須正確設置格式,以保證長度和其他限制。他們仍將通過與真實用戶的電話號碼相同的驗證。
- 您最多可以添加10個電話號碼進行開發。
- 使用難以猜測的測試電話號碼/代碼,並經常進行更改。
創建虛構的電話號碼和驗證碼
- 在Firebase控制台中,打開“身份驗證”部分。
- 在“登錄方法”選項卡中,如果尚未啟用,請啟用“電話”提供程序。
- 打開用於測試手風琴的電話號碼菜單。
- 提供您要測試的電話號碼,例如: +1 650-555-3434 。
- 提供該特定號碼的6位驗證碼,例如: 654321 。
- 添加號碼。如果需要,可以將鼠標懸停在相應的行上,然後單擊垃圾桶圖標,以刪除電話號碼及其代碼。
手動測試
您可以在應用程序中直接開始使用虛構的電話號碼。這樣,您就可以在開發階段執行手動測試,而不會遇到配額問題或限制。您也可以直接從iOS模擬器或Android模擬器進行測試,而無需安裝Google Play服務。
當您提供虛擬電話號碼並發送驗證碼時,不會發送任何實際的SMS。相反,您需要提供先前配置的驗證碼才能完成登錄。
登錄完成後,將使用該電話號碼創建一個Firebase用戶。該用戶具有與真實電話號碼用戶相同的行為和屬性,並且可以以相同方式訪問Realtime Database / Cloud Firestore和其他服務。在此過程中鑄造的ID令牌具有與真實電話號碼用戶相同的簽名。
另一個選擇是,如果您想進一步限制訪問權限,則可以通過對這些用戶的自定義聲明來設置測試角色,以將其區分為假用戶。
整合測試
除了手動測試之外,Firebase身份驗證還提供API,以幫助編寫用於電話驗證測試的集成測試。這些API通過禁用Web中的reCAPTCHA要求和iOS中的靜默推送通知來禁用應用程序驗證。這使得在這些流程中進行自動化測試成為可能,並且更易於實現。此外,它們還有助於提供在Android上測試即時驗證流程的功能。
在iOS上,該appVerificationDisabledForTesting
設置必須被設置為TRUE
調用之前verifyPhoneNumber
。無需任何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 }]; }];
目標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; }]; }];
附錄:使用電話登錄而不會產生麻煩
Firebase身份驗證使用方法轉換來自動獲取應用程序的APNs令牌,處理Firebase發送給您的應用程序的靜默推送通知,並在驗證過程中自動攔截來自reCAPTCHA驗證頁面的自定義方案重定向。
如果您不想使用swizzling,則可以通過將標誌FirebaseAppDelegateProxyEnabled
添加到應用程序的Info.plist文件並將其設置為NO
來禁用它。請注意,將此標誌設置為“ NO
還會禁用包括Firebase Cloud Messaging在內的其他Firebase產品的混亂。
如果您禁用漫遊,則必須將APNs設備令牌,推送通知和自定義方案重定向URL明確傳遞給Firebase身份驗證。
要獲取APNs設備令牌,請實現application:didRegisterForRemoteNotificationsWithDeviceToken:
方法,然後在其中將設備令牌傳遞給FIRAuth
的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 // ... }
目標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:
方法中,通過調用FIRAuth
的canHandleNotification:
方法檢查與Firebase FIRAuth
相關的通知。
迅速
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, developer should handle it. }
目標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, developer should handle it. }
要處理自定義方案重定向URL,請為運行iOS 8及更高版本的設備實現application:openURL:sourceApplication:annotation:
方法,為運行iOS 9及更高版本的設備實現application:openURL:options:
方法,並在其中傳遞FIRAuth
的canHandleURL
方法的URL。
迅速
// For iOS 9+ func application(_ application: UIApplication, open url: URL, options: [UIApplicationOpenURLOptionsKey : Any]) -> Bool { if Auth.auth().canHandle(url) { return true } // URL not auth related, developer should handle it. } // For iOS 8- func application(_ application: UIApplication, open url: URL, sourceApplication: String?, annotation: Any) -> Bool { if Auth.auth().canHandle(url) { Return true } // URL not auth related, developer should handle it. }
目標C
// For iOS 9+ - (BOOL)application:(UIApplication *)app openURL:(NSURL *)url options:(NSDictionary<UIApplicationOpenURLOptionsKey, id> *)options { if ([[FIRAuth auth] canHandleURL:url]) { return YES; } // URL not auth related, developer should handle it. } // For iOS 8- - (BOOL)application:(UIApplication *)application openURL:(NSURL *)url sourceApplication:(NSString *)sourceApplication annotation:(id)annotation { if ([[FIRAuth auth] canHandleURL:url]) { return YES; } // URL not auth related, developer should handle it. }
如果您使用SwiftUI或UISceneDelegate
來處理重定向URL,請實現scene:openURLContexts:
方法,然後在其中將URL傳遞給FIRAuth
的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, developer should handle it. }
物鏡
- (void)scene:(UIScene *)scene openURLContexts:(NSSet<UIOpenURLContext *> *)URLContexts { for (UIOpenURLContext *urlContext in URLContexts) { [FIRAuth.auth canHandleURL:urlContext.url]; // URL not auth related, developer should handle it. } }
下一步
用戶首次登錄後,將創建一個新的用戶帳戶並將其鏈接到該用戶登錄的憑據(即用戶名和密碼,電話號碼或身份驗證提供者信息)。此新帳戶存儲為Firebase項目的一部分,無論用戶如何登錄,都可以用來在項目中的每個應用程序中識別用戶。
在Firebase實時數據庫和雲存儲安全規則中,您可以從
auth
變量中獲取登錄用戶的唯一用戶ID,然後使用它來控制用戶可以訪問哪些數據。
通過將身份驗證提供程序憑據鏈接到現有用戶帳戶,可以允許用戶使用多個身份驗證提供程序登錄您的應用程序。
要註銷用戶,請致電signOut:
迅速
let firebaseAuth = Auth.auth() do { try firebaseAuth.signOut() } catch let signOutError as NSError { print ("Error signing out: %@", signOutError) }
目標C
NSError *signOutError; BOOL status = [[FIRAuth auth] signOut:&signOutError]; if (!status) { NSLog(@"Error signing out: %@", signOutError); return; }
您可能還想為所有身份驗證錯誤添加錯誤處理代碼。請參閱處理錯誤。