使用 Apple 进行身份验证 (JavaScript)

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

准备工作

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

加入 Apple Developer Program

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

配置“通过 Apple 登录”

Apple Developer 网站上,执行以下操作:

  1. 按照为网页配置“通过 Apple 登录”的第一部分所述,将您的网站关联至您的应用。出现提示时,将以下网址注册为返回网址:

    https://YOUR_FIREBASE_PROJECT_ID.firebaseapp.com/__/auth/handler

    您可以通过 Firebase 控制台设置页面获取您的 Firebase 项目 ID。

    完成后,请记下新的服务 ID,下一部分需要用到该 ID。

  2. 创建“通过 Apple 登录”的私钥。下一部分需要用到您的新私钥和密钥 ID。
  3. 如果您会使用任何将向用户发送电子邮件的 Firebase Authentication 功能,包括电子邮件链接登录、电子邮件地址验证、帐号更改撤消等功能,请配置 Apple 私人电子邮件中继服务并注册 noreply@YOUR_FIREBASE_PROJECT_ID.firebaseapp.com(或您的自定义电子邮件模板网域),以便 Apple 可以将 Firebase Authentication 所发送的电子邮件中继到匿名化的 Apple 电子邮件地址。

启用 Apple 作为登录服务提供方

  1. 将 Firebase 添加到您的项目
  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 的要求。

使用 Firebase SDK 处理登录流程

如果您是在构建 Web 应用,想要通过用户的 Apple 帐号对其进行 Firebase 身份验证,最简单的方法是使用 Firebase JavaScript SDK 来处理登录流程。

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

  1. 使用相应的提供方 ID apple.com 创建 OAuthProvider 实例。

    Web 模块化 API

    import { OAuthProvider } from "firebase/auth";
    
    const provider = new OAuthProvider('apple.com');

    Web 命名空间型 API

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

    Web 模块化 API

    provider.addScope('email');
    provider.addScope('name');

    Web 命名空间型 API

    provider.addScope('email');
    provider.addScope('name');

    默认情况下,如果启用了每个电子邮件地址一个帐号,Firebase 会申请电子邮件和名称范围。如果您将此设置更改为每个电子邮件地址多个帐号,那么除非您自行指定,否则 Firebase 不会向 Apple 申请任何范围。

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

    Web 模块化 API

    provider.setCustomParameters({
      // Localize the Apple authentication screen in French.
      locale: 'fr'
    });

    Web 命名空间型 API

    provider.setCustomParameters({
      // Localize the Apple authentication screen in French.
      locale: 'fr'
    });
  4. 使用 OAuth 提供方对象进行 Firebase 身份验证。您可以打开弹出式窗口或将用户重定向至登录页面,来提示用户使用其 Apple 帐号登录。在移动设备上最好使用重定向方法。

    • 如需使用弹出式窗口登录,请调用 signInWithPopup()

      Web 模块化 API

      import { getAuth, signInWithPopup, OAuthProvider } from "firebase/auth";
      
      const auth = getAuth();
      signInWithPopup(auth, provider)
        .then((result) => {
          // The signed-in user info.
          const user = result.user;
      
          // Apple credential
          const credential = OAuthProvider.credentialFromResult(result);
          const accessToken = credential.accessToken;
          const idToken = credential.idToken;
      
          // IdP data available using getAdditionalUserInfo(result)
          // ...
        })
        .catch((error) => {
          // Handle Errors here.
          const errorCode = error.code;
          const errorMessage = error.message;
          // The email of the user's account used.
          const email = error.customData.email;
          // The credential that was used.
          const credential = OAuthProvider.credentialFromError(error);
      
          // ...
        });

      Web 命名空间型 API

      firebase
        .auth()
        .signInWithPopup(provider)
        .then((result) => {
          /** @type {firebase.auth.OAuthCredential} */
          var credential = result.credential;
      
          // The signed-in user info.
          var user = result.user;
      
          // You can also get the Apple OAuth Access and ID Tokens.
          var accessToken = credential.accessToken;
          var idToken = credential.idToken;
      
          // IdP data available using getAdditionalUserInfo(result)
        // ...
        })
        .catch((error) => {
          // Handle Errors here.
          var errorCode = error.code;
          var errorMessage = error.message;
          // The email of the user's account used.
          var email = error.email;
          // The firebase.auth.AuthCredential type that was used.
          var credential = error.credential;
      
          // ...
        });
    • 如需重定向到登录页面进行登录,请调用 signInWithRedirect()

    使用 signInWithRedirectlinkWithRedirectreauthenticateWithRedirect 时,请遵循最佳实践

    Web 模块化 API

    import { getAuth, signInWithRedirect } from "firebase/auth";
    
    const auth = getAuth();
    signInWithRedirect(auth, provider);

    Web 命名空间型 API

    firebase.auth().signInWithRedirect(provider);

    用户完成登录并返回到页面后,您可以调用 getRedirectResult() 获取登录结果:

    Web 模块化 API

    import { getAuth, getRedirectResult, OAuthProvider } from "firebase/auth";
    
    // Result from Redirect auth flow.
    const auth = getAuth();
    getRedirectResult(auth)
      .then((result) => {
        const credential = OAuthProvider.credentialFromResult(result);
        if (credential) {
          // You can also get the Apple OAuth Access and ID Tokens.
          const accessToken = credential.accessToken;
          const idToken = credential.idToken;
        }
        // The signed-in user info.
        const user = result.user;
      })
      .catch((error) => {
        // Handle Errors here.
        const errorCode = error.code;
        const errorMessage = error.message;
        // The email of the user's account used.
        const email = error.customData.email;
        // The credential that was used.
        const credential = OAuthProvider.credentialFromError(error);
    
        // ...
      });

    Web 命名空间型 API

    // Result from Redirect auth flow.
    firebase
      .auth()
      .getRedirectResult()
      .then((result) => {
        if (result.credential) {
          /** @type {firebase.auth.OAuthCredential} */
          var credential = result.credential;
    
          // You can get the Apple OAuth Access and ID Tokens.
          var accessToken = credential.accessToken;
          var idToken = credential.idToken;
    
          // IdP data available in result.additionalUserInfo.profile.
          // ...
        }
        // The signed-in user info.
        var user = result.user;
      })
      .catch((error) => {
        // Handle Errors here.
        var errorCode = error.code;
        var errorMessage = error.message;
        // The email of the user's account used.
        var email = error.email;
        // The firebase.auth.AuthCredential type that was used.
        var credential = error.credential;
    
        // ...
      });

    您还可以在此处捕获并处理错误。如需查看错误代码列表,请参阅 API 参考文档

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

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

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

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

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

Web 模块化 API

import { getAuth, reauthenticateWithPopup, OAuthProvider } from "firebase/auth";

// Result from Redirect auth flow.
const auth = getAuth();
const provider = new OAuthProvider('apple.com');

reauthenticateWithPopup(auth.currentUser, provider)
  .then((result) => {
    // User is re-authenticated with fresh tokens minted and can perform
    // sensitive operations like account deletion, or updating their email
    // address or password.

    // The signed-in user info.
    const user = result.user;

    // You can also get the Apple OAuth Access and ID Tokens.
    const credential = OAuthProvider.credentialFromResult(result);
    const accessToken = credential.accessToken;
    const idToken = credential.idToken;

    // ...
  })
  .catch((error) => {
    // Handle Errors here.
    const errorCode = error.code;
    const errorMessage = error.message;
    // The email of the user's account used.
    const email = error.customData.email;
    // The credential that was used.
    const credential = OAuthProvider.credentialFromError(error);

    // ...
  });

Web 命名空间型 API

const provider = new firebase.auth.OAuthProvider('apple.com');

firebase
  .auth()
  .currentUser
  .reauthenticateWithPopup(provider)
  .then((result) => {
    // User is re-authenticated with fresh tokens minted and can perform
    // sensitive operations like account deletion, or updating their email
    // address or password.
    /** @type {firebase.auth.OAuthCredential} */
    var credential = result.credential;

    // The signed-in user info.
    var user = result.user;
     // You can also get the Apple OAuth Access and ID Tokens.
    var accessToken = credential.accessToken;
    var idToken = credential.idToken;

    // IdP data available in result.additionalUserInfo.profile.
      // ...
  })
  .catch((error) => {
    // Handle Errors here.
    var errorCode = error.code;
    var errorMessage = error.message;
    // The email of the user's account used.
    var email = error.email;
    // The firebase.auth.AuthCredential type that was used.
    var credential = error.credential;

    // ...
  });

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

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

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

Web 模块化 API

import { getAuth, linkWithPopup, FacebookAuthProvider } from "firebase/auth";

const auth = getAuth();
const provider = new FacebookAuthProvider();
provider.addScope('user_birthday');

// Assuming the current user is an Apple user linking a Facebook provider.
linkWithPopup(auth.currentUser, provider)
    .then((result) => {
      // Facebook credential is linked to the current Apple user.
      // ...

      // The user can now sign in to the same account
      // with either Apple or Facebook.
    })
    .catch((error) => {
      // Handle error.
    });

Web 命名空间型 API

const provider = new firebase.auth.FacebookAuthProvider();
provider.addScope('user_birthday');

// Assuming the current user is an Apple user linking a Facebook provider.
firebase.auth().currentUser.linkWithPopup(provider)
    .then((result) => {
      // Facebook credential is linked to the current Apple user.
      // Facebook additional data available in result.additionalUserInfo.profile,

      // Additional Facebook OAuth access token can also be retrieved.
      // result.credential.accessToken

      // The user can now sign in to the same account
      // with either Apple or Facebook.
    })
    .catch((error) => {
      // Handle error.
    });

在 Chrome 扩展程序中进行 Firebase 身份验证

如果您是在构建 Chrome 扩展程序应用,则必须添加 Chrome 扩展程序 ID:

  1. Firebase 控制台中打开您的项目。
  2. Authentication 部分中,打开登录方法页面。
  3. 向“已获授权的网域”列表添加格式如下的 URI:
    chrome-extension://CHROME_EXTENSION_ID

由于 Chrome 扩展程序无法使用 HTTP 重定向,因此只能使用弹出式操作方式(signInWithPopuplinkWithPopupreauthenticateWithPopup)。您应该从后台页面脚本而不是弹出式窗口(浏览器操作)中调用这些方法,因为身份验证的弹出式窗口将取消浏览器操作的弹出式窗口。弹出式操作方式只能在使用 Manifest V2 的扩展程序中使用。较新的 Manifest V3 只允许采用 Service Worker 形式的后台脚本,无法执行弹出式操作方式。

在 Chrome 扩展程序的清单文件中,确保将 https://apis.google.com 网址加入 content_security_policy 许可名单中。

请注意,与默认的 firebaseapp.com 网域类似,您仍必须通过 Apple 验证自定义网域:

http://auth.custom.example.com/.well-known/apple-developer-domain-association.txt

高级:在 Node.js 中进行 Firebase 身份验证

要在 Node.js 应用中进行 Firebase 身份验证,请按以下步骤操作:

  1. 使用 Apple 帐号将用户登录并获取用户的 Apple ID 令牌。为此,您可以采取多种方式。例如,如果您的 Node.js 应用具有浏览器前端,请执行下列操作:

    1. 在后端生成随机字符串(“随机数”)并计算其 SHA256 哈希值。随机数是一种一次性使用的值,可用于验证后端与 Apple 身份验证服务器之间的单次往返。

      Web 模块化 API

      const crypto = require("crypto");
      const string_decoder = require("string_decoder");
      
      // Generate a new random string for each sign-in
      const generateNonce = (length) => {
        const decoder = new string_decoder.StringDecoder("ascii");
        const buf = Buffer.alloc(length);
        let nonce = "";
        while (nonce.length < length) {
          crypto.randomFillSync(buf);
          nonce = decoder.write(buf);
        }
        return nonce.slice(0, length);
      };
      
      const unhashedNonce = generateNonce(10);
      
      // SHA256-hashed nonce in hex
      const hashedNonceHex = crypto.createHash('sha256')
        .update(unhashedNonce).digest().toString('hex');

      Web 命名空间型 API

      const crypto = require("crypto");
      const string_decoder = require("string_decoder");
      
      // Generate a new random string for each sign-in
      const generateNonce = function(length) {
        const decoder = new string_decoder.StringDecoder("ascii");
        const buf = Buffer.alloc(length);
        var nonce = "";
        while (nonce.length < length) {
          crypto.randomFillSync(buf);
          nonce = decoder.write(buf);
        }
        return nonce.slice(0, length);
      };
      
      const unhashedNonce = generateNonce(10);
      
      // SHA256-hashed nonce in hex
      const hashedNonceHex = crypto.createHash('sha256')
        .update(unhashedNonce).digest().toString('hex');
    2. 在登录页面上的“通过 Apple 登录”配置中指定经过哈希处理的随机数:

      <script src="https://appleid.cdn-apple.com/appleauth/static/jsapi/appleid/1/en_US/appleid.auth.js"></script>
      <div id="appleid-signin" data-color="black" data-border="true" data-type="sign in"></div>
      <script>
          AppleID.auth.init({
              clientId: YOUR_APPLE_CLIENT_ID,
              scope: 'name email',
              redirectURI: URL_TO_YOUR_REDIRECT_HANDLER,  // See the next step.
              state: '[STATE]',  // Optional value that Apple will send back to you
                                 // so you can return users to the same context after
                                 // they sign in.
              nonce: HASHED_NONCE  // The hashed nonce you generated in the previous step.
          });
      </script>
      
    3. 从 POSTed 身份验证响应服务器端获取 Apple ID 令牌:

      app.post('/redirect', (req, res) => {
        const savedState = req.cookies.__session;
        const code = req.body.code;
        const state = req.body.state;
        const appleIdToken = req.body.id_token;
        if (savedState !== state || !code) {
          res.status(403).send('403: Permission denied');
        } else {
          // Sign in with Firebase using appleIdToken. (See next step).
        }
      });
      

    另请参阅为网页配置“通过 Apple 登录”

  2. 取得用户的 Apple ID 令牌之后,使用该令牌构建凭据对象,然后使用该凭据将用户登录:

    Web 模块化 API

    import { getAuth, signInWithCredential, OAuthProvider } from "firebase/auth";
    
    const auth = getAuth();
    
    // Build Firebase credential with the Apple ID token.
    const provider = new OAuthProvider('apple.com');
    const authCredential = provider.credential({
      idToken: appleIdToken,
      rawNonce: unhashedNonce,
    });
    
    // Sign in with credential form the Apple user.
    signInWithCredential(auth, authCredential)
      .then((result) => {
        // User signed in.
      })
      .catch((error) => {
        // An error occurred. If error.code == 'auth/missing-or-invalid-nonce',
        // make sure you're sending the SHA256-hashed nonce as a hex string
        // with your request to Apple.
        console.log(error);
      });

    Web 命名空间型 API

    // Build Firebase credential with the Apple ID token.
    const provider = new firebase.auth.OAuthProvider('apple.com');
    const authCredential = provider.credential({
      idToken: appleIdToken,
      rawNonce: unhashedNonce,
    });
    
    // Sign in with credential form the Apple user.
    firebase.auth().signInWithCredential(authCredential)
      .then((result) => {
        // User signed in.
      })
      .catch((error) => {
        // An error occurred. If error.code == 'auth/missing-or-invalid-nonce',
        // make sure you're sending the SHA256-hashed nonce as a hex string
        // with your request to Apple.
        console.log(error);
      });

后续步骤

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

  • 在您的应用中,建议通过在 Auth 对象上设置观测者 (observer) 来了解用户的身份验证状态。然后,您便可从 User 对象获取用户的基本个人资料信息。请参阅管理用户

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

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

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

Web 模块化 API

import { getAuth, signOut } from "firebase/auth";

const auth = getAuth();
signOut(auth).then(() => {
  // Sign-out successful.
}).catch((error) => {
  // An error happened.
});

Web 命名空间型 API

firebase.auth().signOut().then(() => {
  // Sign-out successful.
}).catch((error) => {
  // An error happened.
});