使用 Apple 和 C++ 进行身份验证

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

准备工作

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

加入 Apple Developer Program

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

配置“通过 Apple 登录”

您必须在 Firebase 项目中启用并正确配置 Apple 登录。Android 平台与 Apple 平台的配置不同。请先按照 Apple 平台和/或 Android 指南中的“配置‘通过 Apple 登录’功能”部分操作,然后再继续。

启用 Apple 作为登录提供方

  1. Firebase 控制台中,打开 Auth 部分。在登录方法标签页中,启用 Apple 提供方。
  2. 配置 Apple 登录提供方设置:
    1. 如果您仅在 Apple 平台上部署应用,则可以将“服务 ID”、“Apple 团队 ID”、“私钥”和“密钥 ID”字段留空。
    2. 对于 Android 设备的支持:
      1. 将 Firebase 添加到您的 Android 项目。当您在 Firebase 控制台中设置应用时,请务必注册应用的 SHA-1 签名。
      2. Firebase 控制台中,打开 Auth 部分。在登录方法标签页中,启用 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 的要求。

访问 firebase::auth::Auth

Auth 类是所有 API 调用都需要通过的门户。
  1. 添加 Auth 和 App 头文件:
    #include "firebase/app.h"
    #include "firebase/auth.h"
  2. 在您的初始化代码中,创建一个 firebase::App 类。
    #if defined(__ANDROID__)
      firebase::App* app =
          firebase::App::Create(firebase::AppOptions(), my_jni_env, my_activity);
    #else
      firebase::App* app = firebase::App::Create(firebase::AppOptions());
    #endif  // defined(__ANDROID__)
  3. 获取您的 firebase::App 对应的 firebase::auth::Auth 类。AppAuth 是一对一的映射关系。
    firebase::auth::Auth* auth = firebase::auth::Auth::GetAuth(app);

使用 Firebase SDK 处理登录流程

“通过 Apple 登录”的过程因 Apple 和 Android 平台而异。

在 Apple 平台上

通过从 C++ 代码调用的 Apple 登录 Objective-C SDK 对用户进行 Firebase 身份验证。

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

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

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

  2. 启动 Apple 的登录流程,在请求中包含 Nonce 的 SHA256 哈希值以及将处理 Apple 响应的代理类(请参阅下一步):

      - (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];
      }
    
      - (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;
      }
    
  3. 在 ASAuthorizationControllerDelegate` 实现中处理 Apple 的响应。如果登录成功,请使用 Apple 响应中的 ID 令牌和未经哈希处理的 Nonce 来进行 Firebase 身份验证:

      - (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);
          }
        }
    
  4. 使用生成的令牌字符串和原始 Nonce 构建 Firebase 凭据并登录 Firebase。

    firebase::auth::OAuthProvider::GetCredential(
            /*provider_id=*/"apple.com", token, nonce,
            /*access_token=*/nullptr);
    
    firebase::Future<firebase::auth::AuthResult> result =
        auth->SignInAndRetrieveDataWithCredential(credential);
    
  5. 上述模式同样适用于 Reauthenticate,它可用来为要求用户必须有近期登录才能执行的敏感操作检索新的凭据。

    firebase::Future<firebase::auth::AuthResult> result =
        user->Reauthenticate(credential);
    
  6. 您还可利用上述模式将某个账号与 Apple 登录相关联。 但是,如果已有 Firebase 账号与该 Apple 账号相关联,您再尝试关联的话就可能会遇到错误。 如果发生这种情况,Future 将会返回 kAuthErrorCredentialAlreadyInUse 状态,并且 AuthResult 可能包含有效的 credential。您可以使用此凭据通过 SignInAndRetrieveDataWithCredential 登录与 Apple 关联的账号,而无需生成其他 Apple 登录令牌和 Nonce。

    firebase::Future<firebase::auth::AuthResult> link_result =
        auth->current_user().LinkWithCredential(credential);
    
    // To keep example simple, wait on the current thread until call completes.
    while (link_result.status() == firebase::kFutureStatusPending) {
      Wait(100);
    }
    
    // Determine the result of the link attempt
    if (link_result.error() == firebase::auth::kAuthErrorNone) {
      // user linked correctly.
    } else if (link_result.error() ==
                   firebase::auth::kAuthErrorCredentialAlreadyInUse &&
               link_result.result()
                   ->additional_user_info.updated_credential.is_valid()) {
      // Sign In with the new credential
      firebase::Future<firebase::auth::AuthResult> result =
          auth->SignInAndRetrieveDataWithCredential(
              link_result.result()->additional_user_info.updated_credential);
    } else {
      // Another link error occurred.
    }

在 Android 设备上

在 Android 上,您可以使用 Firebase SDK 将基于 Web 的通用 OAuth 登录机制集成到您的应用中来执行端到端登录流程,从而对用户进行 Firebase 身份验证。

如需使用 Firebase SDK 处理登录流程,请按以下步骤操作:

  1. 构建一个配置了适用于 Apple 的提供方 ID 的 FederatedOAuthProviderData 实例。

    firebase::auth::FederatedOAuthProviderData provider_data("apple.com");
    
  2. 可选:指定您希望向身份验证提供方申请的超出默认范围的额外 OAuth 2.0 范围。

    provider_data.scopes.push_back("email");
    provider_data.scopes.push_back("name");
    
  3. 可选:如果想要以英文以外的语言显示 Apple 的登录屏幕,请设置 locale 参数。如需了解受支持的语言区域,请查看“通过 Apple 登录”文档

    // Localize to French.
    provider_data.custom_parameters["language"] = "fr";
    ```
    
  4. 提供方数据配置完成后,使用它来创建 FederatedOAuthProvider。

    // Construct a FederatedOAuthProvider for use in Auth methods.
    firebase::auth::FederatedOAuthProvider provider(provider_data);
    
  5. 使用 Auth 提供方对象进行 Firebase 身份验证。请注意,与其他 FirebaseAuth 操作不同,此操作会弹出可供用户输入其凭据的网页视图,从而控制您的界面。

    如需启动登录流程,请调用 signInWithProvider

    firebase::Future<firebase::auth::AuthResult> result =
      auth->SignInWithProvider(provider_data);
    

    然后,您的应用可能会等待或注册一个针对 Future 的回调

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

    firebase::Future<firebase::auth::AuthResult> result =
      user.ReauthenticateWithProvider(provider_data);
    

    然后,您的应用可能会等待或注册一个针对 Future 的回调

  7. 此外,您可以使用 LinkWithCredential() 将不同的身份提供方关联至现有账号。

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

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

    // Initialize a Facebook credential with a Facebook access token.
    AuthCredential credential =
        firebase::auth::FacebookAuthProvider.getCredential(token);
    
    // Assuming the current user is an Apple user linking a Facebook provider.
    firebase::Future<firebase::auth::AuthResult> result =
        auth.current_user().LinkWithCredential(credential);
    

通过 Apple 登录的说明

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

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

Apple 只会在用户首次登录时与应用共享用户信息(例如显示名称)。Firebase 通常会在用户首次使用 Apple 账号登录时存储显示名称,您可以使用 current_user().display_name() 获取该显示名称。不过,如果您过去曾在未使用 Firebase 的情况下已通过 Apple 账号将用户登录到应用,则 Apple 不会向 Firebase 提供该用户的显示名称。

后续步骤

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

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

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