Save the date - Google I/O returns May 18-20. Register to get the most out of the digital experience: Build your schedule, reserve space, participate in Q&As, earn Google Developer profile badges, and more. Register now
本頁面由 Cloud Translation API 翻譯而成。
Switch to English

使用電話號碼在iOS上通過Firebase進行身份驗證

您可以使用Firebase身份驗證通過將SMS消息發送到用戶的電話來登錄用戶。用戶使用SMS消息中包含的一次性代碼登錄。

將電話號碼登錄添加到您的應用程序最簡單的方法是使用FirebaseUI ,它包括一個插入式登錄小部件,該小部件實現了電話號碼登錄的登錄流程以及基於密碼的聯合登錄-在。本文檔介紹瞭如何使用Firebase SDK實施電話號碼登錄流程。

在你開始之前

  1. 將Firebase添加到您的iOS項目
  2. 在您的Podfile包括以下Pod:
    pod 'Firebase/Auth'
    
  3. 如果您尚未將應用程序連接到Firebase項目,請從Firebase控制台進行

安全問題

僅使用電話號碼進行身份驗證雖然方便,但比其他可用方法安全性低,因為擁有電話號碼的身份可以在用戶之間輕鬆轉移。同樣,在具有多個用戶配置文件的設備上,任何可以接收SMS消息的用戶都可以使用設備的電話號碼登錄到帳戶。

如果您在應用中使用基於電話號碼的登錄,則應在提供更安全的登錄方法的同時提供它,並告知用戶使用電話號碼登錄的安全權衡。

為您的Firebase項目啟用電話號碼登錄

要通過SMS登錄用戶,您必須首先為Firebase項目啟用電話號碼登錄方法:

  1. Firebase控制台中,打開“身份驗證”部分。
  2. 在“登錄方法”頁面上,啟用“電話號碼”登錄方法。

Firebase的電話號碼登錄請求配額足夠高,因此大多數應用程序都不會受到影響。但是,如果需要使用電話身份驗證登錄大量用戶,則可能需要升級定價計劃。請參閱定價頁面。

啟用應用驗證

要使用電話號碼身份驗證,Firebase必須能夠驗證電話號碼登錄請求是否來自您的應用程序。 Firebase身份驗證可通過兩種方式完成此任務:

  • 靜默APN通知:當您首次在設備上使用其電話號碼登錄用戶時,Firebase身份驗證會使用靜默推送通知將令牌發送到設備。如果您的應用成功從Firebase接收到通知,則可以繼續進行電話號碼登錄。

    對於iOS 8.0及更高版本,靜默通知不需要用戶的明確同意,因此不受拒絕在應用程序中接收APNs通知的用戶的影響。因此,在實施Firebase電話號碼身份驗證時,該應用無需請求用戶權限即可接收推送通知。

  • reCAPTCHA驗證:如果無法發送或接收靜默推送通知,例如當用戶禁用了應用程序的後台刷新,或者在iOS模擬器上測試應用程序時,Firebase身份驗證將使用reCAPTCHA驗證來完成手機登錄流程。 reCAPTCHA挑戰通常可以在無需用戶解決任何問題的情況下完成。

正確配置靜默推送通知後,只有極少數用戶會遇到reCAPTCHA流程。但是,無論是否有靜默推送通知,您都應確保電話號碼登錄功能正常。

開始接收靜默通知

啟用APNs通知以與Firebase身份驗證一起使用:

  1. 在Xcode中,為您的項目啟用推送通知
  2. 將您的APNs身份驗證密鑰上載到Firebase。如果您還沒有APNs身份驗證密鑰,請參閱使用FCM配置APNs

    1. 在Firebase控制台的項目內部,選擇齒輪圖標,選擇Project Settings ,然後選擇Cloud Messaging選項卡。

    2. iOS應用配置下的APNs身份驗證密鑰中,單擊“上載”按鈕。

    3. 瀏覽到保存密鑰的位置,選擇它,然後單擊“打開” 。添加密鑰的密鑰ID(可在Apple Developer Member Center中的證書,標識符和配置文件中找到),然後點擊上傳

    如果您已經擁有APNs證書,則可以上載該證書。

設置reCAPTCHA驗證

要使Firebase SDK能夠使用reCAPTCHA驗證,請執行以下操作:

  1. 將自定義URL方案添加到您的Xcode項目中:
    1. 打開項目配置:在左樹視圖中雙擊項目名稱。從“目標”部分中選擇您的應用,然後選擇“信息”選項卡,然後展開“ URL類型”部分。
    2. 單擊+按鈕,然後為您的反向客戶端ID添加URL方案。要找到此值,請打開GoogleService-Info.plist配置文件,然後查找REVERSED_CLIENT_ID密鑰。複製該鍵的值,然後將其粘貼到配置頁上的“ URL方案”框中。將其他字段留空。

      完成後,您的配置應類似於以下內容(但具有特定於應用程序的值):

  2. 可選:如果要自定義應用程序在向用戶顯示reCAPTCHA時呈現SFSafariViewControllerUIWebView的方式,請創建一個符合FIRAuthUIDelegate協議的自定義類,並將其傳遞給verifyPhoneNumber:UIDelegate:completion:

將驗證碼發送到用戶的手機

要啟動電話號碼登錄,請向用戶顯示一個界​​面,提示他們提供他們的電話號碼,然後調用verifyPhoneNumber:UIDelegate:completion:要求Firebase通過SMS將驗證碼發送到用戶的電話:

  1. 獲取用戶的電話號碼。

    法律要求各不相同,但是作為最佳實踐並為用戶設定期望,您應該告知他們,如果他們使用電話登錄,則他們可能會收到SMS消息以進行驗證,並採用標準費率。

  2. 調用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
      // ...
    }

    物鏡

    [[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";
    

    物鏡

     // Change language code to french.
     [FIRAuth auth].languageCode = @"fr";
    
  3. 保存驗證ID並在應用加載時將其還原。這樣,如果您的應用在用戶完成登錄流程之前(例如,切換到SMS應用時)終止,則可以確保您仍然具有有效的驗證ID。

    您可以使用任何方式保留驗證ID。一種簡單的方法是使用NSUserDefaults對象保存驗證ID:

    迅速

    UserDefaults.standard.set(verificationID, forKey: "authVerificationID")
    

    物鏡

    NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
    [defaults setObject:verificationID forKey:@"authVerificationID"];
    

    然後,您可以恢復保存的值:

    迅速

    let verificationID = UserDefaults.standard.string(forKey: "authVerificationID")
    

    物鏡

    NSString *verificationID = [defaults stringForKey:@"authVerificationID"];
    

如果對verifyPhoneNumber:UIDelegate:completion:的調用成功,則當用戶在SMS消息中收到驗證碼時,可以提示用戶鍵入驗證碼。

使用驗證碼登錄用戶

用戶向您的應用程序提供SMS消息中的驗證碼後,通過根據驗證碼和驗證ID創建FIRPhoneAuthCredential對象並將該對signInWithCredential:completion:傳遞給signInWithCredential:completion:signInWithCredential:completion:

  1. 從用戶那裡獲取驗證碼。
  2. 根據驗證碼和驗證ID創建FIRPhoneAuthCredential對象。

    迅速

    let credential = PhoneAuthProvider.provider().credential(
        withVerificationID: verificationID,
        verificationCode: verificationCode)

    物鏡

    FIRAuthCredential *credential = [[FIRPhoneAuthProvider provider]
        credentialWithVerificationID:verificationID
                    verificationCode:userInput];
  3. 使用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審閱過程中被拒絕的風險。
  • 無需任何額外的努力即可在開發環境中輕鬆進行測試,例如可以在iOS模擬器或沒有Google Play服務的Android模擬器中進行開發。
  • 編寫集成測試,而不會受到通常在生產環境中應用於真實電話號碼的安全檢查的阻止。

虛構電話號碼必須符合以下要求:

  1. 確保您使用的電話號碼確實是虛構的,並且尚不存在。 Firebase身份驗證不允許您將真實用戶使用的現有電話號碼設置為測試號碼。一種選擇是使用555個帶前綴的號碼作為美國測試電話號碼,例如: +1 650-555-3434
  2. 電話號碼必須正確設置格式,以保證長度和其他限制。他們仍將通過與真實用戶的電話號碼相同的驗證。
  3. 您最多可以添加10個電話號碼進行開發。
  4. 使用難以猜測的測試電話號碼/代碼,並經常進行更改。

創建虛構的電話號碼和驗證碼

  1. Firebase控制台中,打開“身份驗證”部分。
  2. 在“登錄方法”選項卡中,如果尚未啟用,請啟用“電話”提供程序。
  3. 打開“電話號碼”以測試手風琴菜單。
  4. 提供您要測試的電話號碼,例如: +1 650-555-3434
  5. 提供該特定號碼的6位數驗證碼,例如: 654321
  6. 添加號碼。如果需要,可以將鼠標懸停在相應的行上,然後單擊垃圾桶圖標,以刪除電話號碼及其代碼。

手動測試

您可以在應用程序中直接開始使用虛構的電話號碼。這使您可以在開發階段執行手動測試,而不會遇到配額問題或節流問題。您也可以直接從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
    }];
}];

物鏡

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:方法,然後在其中將設備令牌傳遞給FIRAuthsetAPNSToken: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
  // ...
}

物鏡

- (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:方法中,通過調用FIRAuthcanHandleNotification:方法檢查與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, developer should handle it.
}

物鏡

- (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:方法,並在其中傳遞FIRAuthcanHandleURL方法的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.
}

物鏡

// 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傳遞給FIRAuthcanHandleURL方法。

迅速

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項目的一部分,可用於在項目中的每個應用程序中識別用戶,而無論用戶如何登錄。

  • 在您的應用程序中,您可以從FIRUser對象獲取用戶的基本配置文件信息。請參閱管理用戶

  • 在Firebase實時數據庫和雲存儲安全規則中,您可以從auth變量獲取登錄用戶的唯一用戶ID,並使用它來控制用戶可以訪問哪些數據。

通過將身份驗證提供程序憑據鏈接到現有用戶帳戶,可以允許用戶使用多個身份驗證提供程序登錄您的應用程序

要註銷用戶,請致電signOut:

迅速

    let firebaseAuth = Auth.auth()
do {
  try firebaseAuth.signOut()
} catch let signOutError as NSError {
  print ("Error signing out: %@", signOutError)
}
  

物鏡

    NSError *signOutError;
BOOL status = [[FIRAuth auth] signOut:&signOutError];
if (!status) {
  NSLog(@"Error signing out: %@", signOutError);
  return;
}

您可能還想為所有身份驗證錯誤添加錯誤處理代碼。請參閱處理錯誤