使用 Apple 进行身份验证

您可以让用户使用其 Apple ID 进行 Firebase 身份验证,方法是使用 Firebase SDK 执行端到端 OAuth 2.0 登录流程。

准备工作

如需让用户能够通过 Apple 帐号登录,首先需在 Apple 的开发者网站上配置“通过 Apple 登录”功能,然后启用 Apple 作为您的 Firebase 项目的登录服务提供方。

加入 Apple Developer Program

“通过 Apple 登录”功能只能由 Apple Developer Program 的成员配置。

配置“通过 Apple 登录”

  1. 在 Apple 开发者网站的 Certificates, Identifiers & Profiles 网页上为您的应用启用“通过 Apple 登录”功能。
  2. 按照为网页配置“通过 Apple 登录”的第一部分所述,将您的网站关联至您的应用。出现提示时,将以下网址注册为返回网址:
    https://YOUR_FIREBASE_PROJECT_ID.firebaseapp.com/__/auth/handler
    您可以通过 Firebase 控制台设置页面获取您的 Firebase 项目 ID。完成后,请记下新的服务 ID,下一部分需要用到该 ID。
  3. 创建“通过 Apple 登录”的私钥。下一部分需要用到您的新私钥和密钥 ID。
  4. 如果您使用会向用户发送电子邮件的 Firebase Authentication 功能,包括电子邮件链接登录、电子邮件地址验证、帐号更改撤消等功能,请配置 Apple 私人电子邮件中继服务并注册 noreply@YOUR_FIREBASE_PROJECT_ID.firebaseapp.com(或者您的自定义电子邮件模板网域),以便 Apple 可以将 Firebase Authentication 发送的电子邮件中继到匿名的 Apple 电子邮件地址。

启用 Apple 作为登录提供方

  1. 将 Firebase 添加到您的 Apple 项目。当您在 Firebase 控制台中设置应用时,请务必注册应用的软件包 ID。
  2. Firebase 控制台中,打开 Auth 部分。在 Sign in method(登录方法)标签页中,启用 Apple 提供方。 指定您在上一部分中创建的服务 ID。此外,在 OAuth 代码流程配置部分中,指定您的 Apple 团队 ID 以及您在上一部分中创建的私钥和密钥 ID。

符合 Apple 匿名数据要求

如果在登录时选择“通过 Apple 登录”,用户便可对其数据(包括电子邮件地址)进行匿名化处理。选择此选项的用户会拥有使用 privaterelay.appleid.com 网域的电子邮件地址。当您在应用中使用“通过 Apple 登录”时,必须遵守有关这些匿名化 Apple ID 的任何适用的 Apple 开发者政策或条款。

条款内容包括必须首先征得相关用户的同意,才能将能直接识别的个人信息与匿名化的 Apple ID 相关联。使用 Firebase Authentication 时,可能包括以下操作:

  • 将电子邮件地址关联至匿名化的 Apple ID,或者将匿名化的 Apple ID 关联至电子邮件地址。
  • 将电话号码关联至匿名化的 Apple ID,或者将匿名化的 Apple ID 关联至电话号码。
  • 将非匿名社交凭据(Facebook、Google 等)关联至匿名化的 Apple ID,或者将匿名化的 Apple ID 关联至非匿名社交凭据。

以上所列并非全部操作。请参阅您的开发者帐号对应的“成员资格”部分中的“Apple Developer Program 许可协议”,确保您的应用符合 Apple 的要求。

使用 Apple 帐号登录并进行 Firebase 身份验证

如需进行 Apple 帐号身份验证,请先让用户使用 Apple 的 AuthenticationServices 框架登录其 Apple 帐号,然后使用 Apple 响应中的 ID 令牌来创建一个 Firebase AuthCredential 对象:

  1. 为每个登录请求生成随机字符串 (Nonce),用来确保您获取的 ID 令牌专门用于响应您应用的身份验证请求。此步骤对于防止重放攻击至关重要。

    您可以使用 SecRandomCopyBytes(_:_:_) 生成加密安全 Nonce,如以下示例所示:

    Swift

    private func randomNonceString(length: Int = 32) -> String {
      precondition(length > 0)
      var randomBytes = [UInt8](repeating: 0, count: length)
      let errorCode = SecRandomCopyBytes(kSecRandomDefault, randomBytes.count, &randomBytes)
      if errorCode != errSecSuccess {
        fatalError(
          "Unable to generate nonce. SecRandomCopyBytes failed with OSStatus \(errorCode)"
        )
      }
    
      let charset: [Character] =
        Array("0123456789ABCDEFGHIJKLMNOPQRSTUVXYZabcdefghijklmnopqrstuvwxyz-._")
    
      let nonce = randomBytes.map { byte in
        // Pick a random character from the set, wrapping around if needed.
        charset[Int(byte) % charset.count]
      }
    
      return String(nonce)
    }
    
        

    Objective-C

    // Adapted from https://auth0.com/docs/api-auth/tutorials/nonce#generate-a-cryptographically-random-nonce
    - (NSString *)randomNonce:(NSInteger)length {
      NSAssert(length > 0, @"Expected nonce to have positive length");
      NSString *characterSet = @"0123456789ABCDEFGHIJKLMNOPQRSTUVXYZabcdefghijklmnopqrstuvwxyz-._";
      NSMutableString *result = [NSMutableString string];
      NSInteger remainingLength = length;
    
      while (remainingLength > 0) {
        NSMutableArray *randoms = [NSMutableArray arrayWithCapacity:16];
        for (NSInteger i = 0; i < 16; i++) {
          uint8_t random = 0;
          int errorCode = SecRandomCopyBytes(kSecRandomDefault, 1, &random);
          NSAssert(errorCode == errSecSuccess, @"Unable to generate nonce: OSStatus %i", errorCode);
    
          [randoms addObject:@(random)];
        }
    
        for (NSNumber *random in randoms) {
          if (remainingLength == 0) {
            break;
          }
    
          if (random.unsignedIntValue < characterSet.length) {
            unichar character = [characterSet characterAtIndex:random.unsignedIntValue];
            [result appendFormat:@"%C", character];
            remainingLength--;
          }
        }
      }
    
      return [result copy];
    }
        

    您将通过登录请求发送 Nonce 的 SHA256 哈希值,Apple 将在响应中原封不动传递该值。Firebase 通过对原始 Nonce 进行哈希处理并将其与 Apple 传递的值进行比较,来验证响应。

    Swift

    @available(iOS 13, *)
    private func sha256(_ input: String) -> String {
      let inputData = Data(input.utf8)
      let hashedData = SHA256.hash(data: inputData)
      let hashString = hashedData.compactMap {
        String(format: "%02x", $0)
      }.joined()
    
      return hashString
    }
    
        

    Objective-C

    - (NSString *)stringBySha256HashingString:(NSString *)input {
      const char *string = [input UTF8String];
      unsigned char result[CC_SHA256_DIGEST_LENGTH];
      CC_SHA256(string, (CC_LONG)strlen(string), result);
    
      NSMutableString *hashed = [NSMutableString stringWithCapacity:CC_SHA256_DIGEST_LENGTH * 2];
      for (NSInteger i = 0; i < CC_SHA256_DIGEST_LENGTH; i++) {
        [hashed appendFormat:@"%02x", result[i]];
      }
      return hashed;
    }
        
  2. 启动 Apple 的登录流程,在请求中包含 Nonce 的 SHA256 哈希值以及将处理 Apple 响应的代理类(请参阅下一步):

    Swift

    import CryptoKit
    
    // Unhashed nonce.
    fileprivate var currentNonce: String?
    
    @available(iOS 13, *)
    func startSignInWithAppleFlow() {
      let nonce = randomNonceString()
      currentNonce = nonce
      let appleIDProvider = ASAuthorizationAppleIDProvider()
      let request = appleIDProvider.createRequest()
      request.requestedScopes = [.fullName, .email]
      request.nonce = sha256(nonce)
    
      let authorizationController = ASAuthorizationController(authorizationRequests: [request])
      authorizationController.delegate = self
      authorizationController.presentationContextProvider = self
      authorizationController.performRequests()
    }
    

    Objective-C

    @import CommonCrypto;
    
    - (void)startSignInWithAppleFlow {
      NSString *nonce = [self randomNonce:32];
      self.currentNonce = nonce;
      ASAuthorizationAppleIDProvider *appleIDProvider = [[ASAuthorizationAppleIDProvider alloc] init];
      ASAuthorizationAppleIDRequest *request = [appleIDProvider createRequest];
      request.requestedScopes = @[ASAuthorizationScopeFullName, ASAuthorizationScopeEmail];
      request.nonce = [self stringBySha256HashingString:nonce];
    
      ASAuthorizationController *authorizationController =
          [[ASAuthorizationController alloc] initWithAuthorizationRequests:@[request]];
      authorizationController.delegate = self;
      authorizationController.presentationContextProvider = self;
      [authorizationController performRequests];
    }
    
  3. ASAuthorizationControllerDelegate 实现中处理 Apple 的响应。如果登录成功,请使用 Apple 响应中的 ID 令牌和未经哈希处理的 Nonce 来进行 Firebase 身份验证:

    Swift

    @available(iOS 13.0, *)
    extension MainViewController: ASAuthorizationControllerDelegate {
    
      func authorizationController(controller: ASAuthorizationController, didCompleteWithAuthorization authorization: ASAuthorization) {
        if let appleIDCredential = authorization.credential as? ASAuthorizationAppleIDCredential {
          guard let nonce = currentNonce else {
            fatalError("Invalid state: A login callback was received, but no login request was sent.")
          }
          guard let appleIDToken = appleIDCredential.identityToken else {
            print("Unable to fetch identity token")
            return
          }
          guard let idTokenString = String(data: appleIDToken, encoding: .utf8) else {
            print("Unable to serialize token string from data: \(appleIDToken.debugDescription)")
            return
          }
          // Initialize a Firebase credential, including the user's full name.
          let credential = OAuthProvider.appleCredential(withIDToken: idTokenString,
                                                            rawNonce: nonce,
                                                            fullName: appleIDCredential.fullName)
          // Sign in with Firebase.
          Auth.auth().signIn(with: credential) { (authResult, error) in
            if error {
              // Error. If error.code == .MissingOrInvalidNonce, make sure
              // you're sending the SHA256-hashed nonce as a hex string with
              // your request to Apple.
              print(error.localizedDescription)
              return
            }
            // User is signed in to Firebase with Apple.
            // ...
          }
        }
      }
    
      func authorizationController(controller: ASAuthorizationController, didCompleteWithError error: Error) {
        // Handle error.
        print("Sign in with Apple errored: \(error)")
      }
    
    }
    

    Objective-C

    - (void)authorizationController:(ASAuthorizationController *)controller
       didCompleteWithAuthorization:(ASAuthorization *)authorization API_AVAILABLE(ios(13.0)) {
      if ([authorization.credential isKindOfClass:[ASAuthorizationAppleIDCredential class]]) {
        ASAuthorizationAppleIDCredential *appleIDCredential = authorization.credential;
        NSString *rawNonce = self.currentNonce;
        NSAssert(rawNonce != nil, @"Invalid state: A login callback was received, but no login request was sent.");
    
        if (appleIDCredential.identityToken == nil) {
          NSLog(@"Unable to fetch identity token.");
          return;
        }
    
        NSString *idToken = [[NSString alloc] initWithData:appleIDCredential.identityToken
                                                  encoding:NSUTF8StringEncoding];
        if (idToken == nil) {
          NSLog(@"Unable to serialize id token from data: %@", appleIDCredential.identityToken);
        }
    
        // Initialize a Firebase credential, including the user's full name.
        FIROAuthCredential *credential = [FIROAuthProvider appleCredentialWithIDToken:IDToken
                                                                             rawNonce:self.appleRawNonce
                                                                             fullName:appleIDCredential.fullName];
    
        // Sign in with Firebase.
        [[FIRAuth auth] signInWithCredential:credential
                                  completion:^(FIRAuthDataResult * _Nullable authResult,
                                               NSError * _Nullable error) {
          if (error != nil) {
            // Error. If error.code == FIRAuthErrorCodeMissingOrInvalidNonce,
            // make sure you're sending the SHA256-hashed nonce as a hex string
            // with your request to Apple.
            return;
          }
          // Sign-in succeeded!
        }];
      }
    }
    
    - (void)authorizationController:(ASAuthorizationController *)controller
               didCompleteWithError:(NSError *)error API_AVAILABLE(ios(13.0)) {
      NSLog(@"Sign in with Apple errored: %@", error);
    }
    

与 Firebase Auth 支持的其他提供方不同,Apple 不提供照片网址。

此外,当用户选择不与应用共享其电子邮件时,Apple 会为该用户提供格式为 xyz@privaterelay.appleid.com 的唯一电子邮件地址来与应用共享。如果您配置了私人电子邮件中继服务,则 Apple 会将发送到匿名地址的电子邮件转发到用户的真实电子邮件地址。

重新进行身份验证和帐号关联

上述模式同样适用于 reauthenticateWithCredential(),对于要求用户必须在近期内登录过才能执行的敏感操作,您可使用它来检索新的凭据。

Swift

// Initialize a fresh Apple credential with Firebase.
let credential = OAuthProvider.credential(
  withProviderID: "apple.com",
  IDToken: appleIdToken,
  rawNonce: rawNonce
)
// Reauthenticate current Apple user with fresh Apple credential.
Auth.auth().currentUser.reauthenticate(with: credential) { (authResult, error) in
  guard error != nil else { return }
  // Apple user successfully re-authenticated.
  // ...
}

Objective-C

FIRAuthCredential *credential = [FIROAuthProvider credentialWithProviderID:@"apple.com",
                                                                   IDToken:appleIdToken,
                                                                  rawNonce:rawNonce];
[[FIRAuth auth].currentUser
    reauthenticateWithCredential:credential
                      completion:^(FIRAuthDataResult * _Nullable authResult,
                                   NSError * _Nullable error) {
  if (error) {
    // Handle error.
  }
  // Apple user successfully re-authenticated.
  // ...
}];

此外,您还可以使用 linkWithCredential() 将不同的身份提供方关联至现有帐号。

请注意,Apple 要求您须征得用户的明确同意,才能将用户的 Apple 帐号关联至其他数据。

通过 Apple 登录时,您无法重复使用一种身份验证凭据来与现有帐号关联。如果您要将“通过 Apple 登录”凭据与其他帐号相关联,则必须先尝试使用旧的“通过 Apple 登录”凭据来关联帐户,然后检查返回的错误以查找新凭据。 新凭据将位于错误的 userInfo 字典中,并可通过 AuthErrorUserInfoUpdatedCredentialKey 键访问。

例如,要将 Facebook 帐号关联至当前的 Firebase 帐号,请使用将用户登录到 Facebook 时获得的访问令牌:

Swift

// Initialize a Facebook credential with Firebase.
let credential = FacebookAuthProvider.credential(
  withAccessToken: AccessToken.current!.tokenString
)
// Assuming the current user is an Apple user linking a Facebook provider.
Auth.auth().currentUser.link(with: credential) { (authResult, error) in
  // Facebook credential is linked to the current Apple user.
  // The user can now sign in with Facebook or Apple to the same Firebase
  // account.
  // ...
}

Objective-C

// Initialize a Facebook credential with Firebase.
FacebookAuthCredential *credential = [FIRFacebookAuthProvider credentialWithAccessToken:accessToken];
// Assuming the current user is an Apple user linking a Facebook provider.
[FIRAuth.auth linkWithCredential:credential completion:^(FIRAuthDataResult * _Nullable authResult, NSError * _Nullable error) {
  // Facebook credential is linked to the current Apple user.
  // The user can now sign in with Facebook or Apple to the same Firebase
  // account.
  // ...
}];

令牌撤消

Apple 要求支持创建帐号的应用必须允许用户在应用内删除其帐号,如 App Store 审核指南中所述。

如需满足此要求,请执行以下步骤:

  1. 确保您已填写“使用 Apple 帐号登录”提供方配置的“服务 ID”和“OAuth 代码流程配置”部分,如配置“使用 Apple 帐号登录”部分所述。

  2. 通过“使用 Apple 帐号登录”创建用户时,Firebase 不会存储用户令牌,因此您必须让用户再次登录,然后才能撤消其令牌并删除帐号。

    Swift

    private func deleteCurrentUser() {
      do {
        let nonce = try CryptoUtils.randomNonceString()
        currentNonce = nonce
        let appleIDProvider = ASAuthorizationAppleIDProvider()
        let request = appleIDProvider.createRequest()
        request.requestedScopes = [.fullName, .email]
        request.nonce = CryptoUtils.sha256(nonce)
    
        let authorizationController = ASAuthorizationController(authorizationRequests: [request])
        authorizationController.delegate = self
        authorizationController.presentationContextProvider = self
        authorizationController.performRequests()
      } catch {
        // In the unlikely case that nonce generation fails, show error view.
        displayError(error)
      }
    }
    
    
  3. ASAuthorizationAppleIDCredential 获取授权代码,并使用该代码调用 Auth.auth().revokeToken(withAuthorizationCode:) 以撤消用户的令牌。

    Swift

    func authorizationController(controller: ASAuthorizationController,
                                 didCompleteWithAuthorization authorization: ASAuthorization) {
      guard let appleIDCredential = authorization.credential as? ASAuthorizationAppleIDCredential
      else {
        print("Unable to retrieve AppleIDCredential")
        return
      }
    
      guard let _ = currentNonce else {
        fatalError("Invalid state: A login callback was received, but no login request was sent.")
      }
    
      guard let appleAuthCode = appleIDCredential.authorizationCode else {
        print("Unable to fetch authorization code")
        return
      }
    
      guard let authCodeString = String(data: appleAuthCode, encoding: .utf8) else {
        print("Unable to serialize auth code string from data: \(appleAuthCode.debugDescription)")
        return
      }
    
      Task {
        do {
          try await Auth.auth().revokeToken(withAuthorizationCode: authCodeString)
          try await user?.delete()
          self.updateUI()
        } catch {
          self.displayError(error)
        }
      }
    }
    
    
  4. 最后,删除用户帐号(以及所有相关数据)

后续步骤

在用户首次登录后,系统会创建一个新的用户帐号,并将其与该用户登录时使用的凭据(即用户名和密码、电话号码或者身份验证提供方信息)相关联。此新帐号存储在您的 Firebase 项目中,无论用户采用何种方式登录,您项目中的每个应用都可以使用此帐号来识别用户。

  • 在您的应用中,您可以从 User 对象获取用户的基本个人资料信息。请参阅管理用户

  • 在您的 Firebase Realtime Database 和 Cloud Storage 安全规则中,您可以从 auth 变量获取已登录用户的唯一用户 ID,然后利用此 ID 来控制用户可以访问哪些数据。

您可以将多个身份验证提供方凭据与一个现有用户帐号关联,让用户可以使用多个身份验证提供方登录您的应用。

如需将用户退出登录,请调用 signOut:

Swift

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;
}

您可能还需要为所有身份验证错误添加错误处理代码。请参阅处理错误