Check out what’s new from Firebase at Google I/O 2022. Learn more

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

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

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

在你開始之前

使用 Swift Package Manager 安裝和管理 Firebase 依賴項。

  1. 在 Xcode 中,打開您的應用項目,導航到File > Add Packages
  2. 出現提示時,添加 Firebase Apple 平台 SDK 存儲庫:
  3.   https://github.com/firebase/firebase-ios-sdk
      
  4. 選擇 Firebase 身份驗證庫。
  5. 完成後,Xcode 將在後台自動開始解析和下載您的依賴項。
並且,檢查配置步驟:
  1. 如果您尚未將應用連接到 Firebase 項目,請從Firebase 控制台執行此操作。

安全問題

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

如果您在應用程序中使用基於電話號碼的登錄,則應將其與更安全的登錄方法一起提供,並告知用戶使用電話號碼登錄的安全權衡。

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

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

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

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

啟用應用驗證

要使用電話號碼身份驗證,Firebase 必須能夠驗證電話號碼登錄請求是否來自您的應用。 Firebase 身份驗證有兩種方法可以完成此操作:

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

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

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

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

開始接收靜默通知

要啟用 APNs 通知以用於 Firebase 身份驗證:

  1. 在 Xcode 中,為您的項目啟用推送通知
  2. 將您的 APNs 身份驗證密鑰上傳到 Firebase。如果您還沒有 APNs 身份驗證密鑰,請確保在Apple Developer Member Center中創建一個。

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

    2. iOS app configuration下的APNs authentication key中,單擊Upload按鈕。

    3. 瀏覽到您保存密鑰的位置,選擇它,然後單擊打開。添加密鑰的密鑰 ID(可在Apple Developer Member Center中獲得),然後單擊Upload

    如果您已經擁有 APNs 證書,則可以改為上傳證書。

設置 reCAPTCHA 驗證

要啟用 Firebase SDK 以使用 reCAPTCHA 驗證:

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

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

  2. 可選:如果您想自定義應用程序在向用戶顯示 reCAPTCHA 時呈現SFSafariViewController的方式,請創建一個符合AuthUIDelegate協議的自定義類,並將其傳遞給verifyPhoneNumber(_:uiDelegate:completion:)

向用戶手機發送驗證碼

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

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

    法律要求各不相同,但作為最佳實踐並為您的用戶設定期望,您應該告知他們,如果他們使用電話登錄,他們可能會收到一條短信進行驗證,並適用標準費率。

  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
          // ...
      }

    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方法將不會發送第二條 SMS,除非原始請求已超時。

    當您調用verifyPhoneNumber(_:uiDelegate:completion:)時,Firebase 會向您的應用發送靜默推送通知,或向用戶發出 reCAPTCHA 質詢。在您的應用收到通知或用戶完成 reCAPTCHA 質詢後,Firebase 會向指定的電話號碼發送一條包含驗證碼的 SMS 消息,並將驗證 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";
    
  3. 保存驗證 ID 並在您的應用加載時恢復它。通過這樣做,如果您的應用在用戶完成登錄流程之前終止(例如,在切換到 SMS 應用時),您可以確保您仍然擁有有效的驗證 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:)調用成功,您可以在用戶收到短信時提示輸入驗證碼。

使用驗證碼登錄用戶

在用戶向您的應用提供 SMS 消息中的驗證碼後,通過從驗證碼和驗證 ID 創建FIRPhoneAuthCredential對象並將該對像傳遞給signInWithCredential:completion:來登錄用戶。

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

    迅速

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

    Objective-C

    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
        // ...
    }

    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 控制台設置用於開發的虛構電話號碼。使用虛構的電話號碼進行測試可帶來以下好處:

  • 在不消耗您的使用配額的情況下測試電話號碼身份驗證。
  • 在不發送實際 SMS 消息的情況下測試電話號碼身份驗證。
  • 使用相同的電話號碼運行連續測試而不會受到限制。如果審核者碰巧使用相同的電話號碼進行測試,這可以最大限度地降低 App Store 審核過程中被拒絕的風險。
  • 無需任何額外工作即可在開發環境中輕鬆進行測試,例如無需 Google Play 服務即可在 iOS 模擬器或 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. 添加號碼。如果需要,您可以通過將鼠標懸停在相應行上並單擊垃圾桶圖標來刪除電話號碼及其代碼。

手動測試

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

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 產品的 swizzling,包括 Firebase Cloud Messaging。

如果禁用 swizzling,則必須將 APNs 設備令牌、推送通知和自定義方案重定向 URL 顯式傳遞給 Firebase 身份驗證。

要獲取 APNs 設備令牌,請實現application(_:didRegisterForRemoteNotificationsWithDeviceToken:)方法,並在其中將設備令牌傳遞給AuthsetAPNSToken(_: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:):方法中,通過調用AuthcanHandleNotification(_:)方法檢查與 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 傳遞給AuthcanHandleURL(_:)方法。

迅速

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 傳遞給AuthcanHandleURL(_:)方法。

迅速

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

  • 在您的應用程序中,您可以從FIRUser對象獲取用戶的基本個人資料信息。請參閱管理用戶

  • 在您的 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;
}

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