Google 致力于为黑人社区推动种族平等。查看具体举措

使用 JavaScript 通过 Apple 进行身份验证

您可以让用户使用其 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 部分。在登录方法标签页上,启用 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 版本 9

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

    Web 版本 8

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

    Web 版本 9

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

    Web 版本 8

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

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

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

    Web 版本 9

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

    Web 版本 8

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

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

      Web 版本 9

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

      Web 版本 8

      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;
      
          // ...
        })
        .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()

      Web 版本 9

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

      Web 版本 8

      firebase.auth().signInWithRedirect(provider);

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

      Web 版本 9

      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.email;
          // The credential that was used.
          const credential = OAuthProvider.credentialFromError(error);
      
          // ...
        });

      Web 版本 8

      // 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;
      
            // ...
          }
          // 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 身份验证支持的其他提供方不同,Apple 不提供照片网址。

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

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

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

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

Web 版本 9

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.email;
    // The credential that was used.
    const credential = OAuthProvider.credentialFromError(error);

    // ...
  });

Web 版本 8

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;

    // ...
  })
  .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 版本 9

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 版本 8

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. 身份验证部分中,打开登录方法页面。
  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 版本 9

      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.substr(0, length);
      };
      
      const unhashedNonce = generateNonce(10);
      
      // SHA256-hashed nonce in hex
      const hashedNonceHex = crypto.createHash('sha256')
        .update(unhashedNonce).digest().toString('hex');

      Web 版本 8

      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.substr(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 版本 9

    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 版本 8

    // 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 对象上设置观察者的方式来了解用户的身份验证状态。然后您可以从 User 对象获取用户的基本个人资料信息。请参阅管理用户

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

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

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

Web 版本 9

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

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

Web 版本 8

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