您可以使用 Firebase 身份驗證,透過向使用者的手機發送簡訊來登入使用者。使用者使用 SMS 訊息中包含的一次性代碼登入。
將電話號碼登入新增至應用程式最簡單的方法是使用FirebaseUI ,其中包括一個下拉登入小部件,該小部件可實現電話號碼登入的登入流程以及基於密碼的聯合登入。本文檔說明如何使用 Firebase SDK 實作電話號碼登入流程。
在你開始之前
使用 Swift Package Manager 安裝和管理 Firebase 相依性。
- 在 Xcode 中,開啟應用程式項目,導覽至File > Add Packages 。
- 出現提示時,新增 Firebase Apple 平台 SDK 儲存庫:
- 選擇 Firebase 身份驗證庫。
- 將
-ObjC
標誌新增至目標建置設定的「其他連結器標誌」部分。 - 完成後,Xcode 將自動開始在背景解析並下載您的依賴項。
https://github.com/firebase/firebase-ios-sdk.git
- 如果您尚未將應用程式連線至 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; }
您可能還需要為所有身份驗證錯誤新增錯誤處理代碼。請參閱處理錯誤。