您可以使用 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 身份驗證可以通過兩種方式實現這一點:
- 靜默 APNs 通知:當您首次在設備上使用用戶的電話號碼登錄時,Firebase 身份驗證會使用靜默推送通知向設備發送令牌。如果您的應用成功收到來自 Firebase 的通知,則可以繼續進行電話號碼登錄。
對於 iOS 8.0 及更新版本,靜默通知不需要明確的用戶同意,因此不受用戶拒絕在應用程序中接收 APNs 通知的影響。因此,應用程序在實施 Firebase 電話號碼身份驗證時無需請求用戶許可即可接收推送通知。
- reCAPTCHA 驗證:在無法發送或接收靜默推送通知的情況下,例如當用戶為您的應用禁用後台刷新時,或者在 iOS 模擬器上測試您的應用時,Firebase 身份驗證使用 reCAPTCHA 驗證來完成手機登錄流程。 reCAPTCHA 挑戰通常可以在用戶無需解決任何問題的情況下完成。
正確配置靜默推送通知後,只有極少數用戶會體驗到 reCAPTCHA 流程。儘管如此,無論靜默推送通知是否可用,您都應確保電話號碼登錄功能正常。
開始接收靜默通知
要啟用 APNs 通知以用於 Firebase 身份驗證:
- 在 Xcode 中,為您的項目啟用推送通知。
將您的 APNs 身份驗證密鑰上傳到 Firebase。如果您還沒有 APNs 身份驗證密鑰,請確保在Apple Developer Member Center中創建一個。
在 Firebase 控制台的項目內,選擇齒輪圖標,選擇Project Settings ,然後選擇Cloud Messaging選項卡。
在iOS app configuration下的APNs authentication key中,點擊Upload按鈕。
瀏覽到您保存密鑰的位置,選擇它,然後單擊打開。添加密鑰的密鑰 ID(可在Apple Developer Member Center中獲得)並單擊Upload 。
如果您已經擁有 APNs 證書,則可以改為上傳證書。
設置 reCAPTCHA 驗證
要使 Firebase SDK 能夠使用 reCAPTCHA 驗證:
- 將自定義 URL 方案添加到您的 Xcode 項目:
- 打開您的項目配置:雙擊左側樹視圖中的項目名稱。從TARGETS部分選擇您的應用程序,然後選擇Info選項卡,並展開URL Types部分。
- 單擊+按鈕,並將您的編碼應用程序 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 // ... }
目標-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
方法將不會發送第二條 SMS,除非原始請求已超時。當您調用
verifyPhoneNumber(_:uiDelegate:completion:)
時,Firebase 會向您的應用發送靜默推送通知,或向用戶發出 reCAPTCHA 質詢。在您的應用收到通知或用戶完成 reCAPTCHA 質詢後,Firebase 會向指定的電話號碼發送一條包含身份驗證代碼的短信,並將驗證 ID 傳遞給您的完成函數。您將需要驗證碼和驗證 ID 才能登錄用戶。Firebase 發送的 SMS 消息也可以通過 Auth 實例上的
languageCode
屬性指定 auth 語言來本地化。迅速
// 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:)
調用成功,您可以提示用戶在短信中收到驗證碼時鍵入驗證碼。
使用驗證碼登錄用戶
在用戶向您的應用程序提供短信中的驗證碼後,通過從驗證碼和驗證 ID 創建一個FIRPhoneAuthCredential
對象並將該對像傳遞給signInWithCredential:completion:
來讓用戶登錄。
- 獲取用戶的驗證碼。
- 從驗證碼和驗證 ID 創建一個
FIRPhoneAuthCredential
對象。迅速
let credential = PhoneAuthProvider.provider().credential( withVerificationID: verificationID, verificationCode: verificationCode )
目標-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 // ... }
目標-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 控制台設置用於開發的虛構電話號碼。使用虛構的電話號碼進行測試具有以下優勢:
- 在不消耗您的使用配額的情況下測試電話號碼身份驗證。
- 在不發送實際 SMS 消息的情況下測試電話號碼身份驗證。
- 使用相同的電話號碼運行連續測試而不會受到限制。如果審閱者碰巧使用相同的電話號碼進行測試,這可以最大限度地降低應用商店審閱過程中被拒絕的風險。
- 無需任何額外工作即可在開發環境中輕鬆進行測試,例如無需 Google Play 服務即可在 iOS 模擬器或 Android 模擬器中進行開發的能力。
- 編寫集成測試,而不會被通常應用於生產環境中真實電話號碼的安全檢查所阻止。
虛構的電話號碼必須滿足以下要求:
- 確保您使用的電話號碼確實是虛構的,並且不存在。 Firebase 身份驗證不允許您將真實用戶使用的現有電話號碼設置為測試號碼。一種選擇是使用 555 前綴號碼作為美國測試電話號碼,例如: +1 650-555-3434
- 電話號碼必鬚根據長度和其他限制正確格式化。他們仍將通過與真實用戶電話號碼相同的驗證。
- 您最多可以添加 10 個電話號碼進行開發。
- 使用難以猜測並經常更改的測試電話號碼/代碼。
創建虛構的電話號碼和驗證碼
- 在Firebase 控制台中,打開身份驗證部分。
- 在“登錄方法”選項卡中,啟用電話提供商(如果尚未啟用)。
- 打開用於測試手風琴菜單的電話號碼。
- 提供您要測試的電話號碼,例如: +1 650-555-3434 。
- 提供該特定號碼的 6 位驗證碼,例如: 654321 。
- 添加號碼。如果需要,您可以通過將鼠標懸停在相應行上並單擊垃圾桶圖標來刪除電話號碼及其代碼。
手動測試
您可以直接開始在您的應用程序中使用虛構的電話號碼。這使您可以在開發階段執行手動測試,而不會遇到配額問題或限制。您還可以在未安裝 Google Play 服務的情況下直接從 iOS 模擬器或 Android 模擬器進行測試。
當您提供虛構的電話號碼並發送驗證碼時,不會發送實際的短信。相反,您需要提供之前配置的驗證碼才能完成登錄。
登錄完成後,將使用該電話號碼創建一個 Firebase 用戶。用戶與真實電話號碼用戶具有相同的行為和屬性,可以通過相同的方式訪問實時數據庫/Cloud Firestore 等服務。在此過程中生成的 ID 令牌與真實電話號碼用戶具有相同的簽名。
如果您想進一步限制訪問,另一種選擇是通過對這些用戶的自定義聲明設置測試角色,以將他們區分為假用戶。
集成測試
除了手動測試之外,Firebase 身份驗證還提供 API 來幫助編寫電話身份驗證測試的集成測試。這些 API 通過禁用 Web 中的 reCAPTCHA 要求和 iOS 中的靜默推送通知來禁用應用程序驗證。這使得在這些流程中進行自動化測試成為可能,並且更容易實施。此外,它們還有助於提供在 Android 上測試即時驗證流程的能力。
在 iOS 上, appVerificationDisabledForTesting
設置必須在調用verifyPhoneNumber
之前設置為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 }]; }];
目標-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 身份驗證使用 method swizzling 自動獲取您應用的 APNs 令牌,處理 Firebase 發送到您應用的靜默推送通知,並在驗證期間自動攔截來自 reCAPTCHA 驗證頁面的自定義方案重定向。
如果您不想使用 swizzling,您可以通過將標誌FirebaseAppDelegateProxyEnabled
添加到應用程序的 Info.plist 文件並將其設置為NO
來禁用它。請注意,將此標誌設置為NO
還會禁用其他 Firebase 產品的調配,包括 Firebase 雲消息傳遞。
如果禁用調配,則必須明確將 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 // ... }
目標-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. }
目標-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. }
目標-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. }
目標-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 Realtime Database 和 Cloud Storage Security Rules中,您可以從
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; }
您可能還想為所有身份驗證錯誤添加錯誤處理代碼。請參閱處理錯誤。