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

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

准备工作

如需让用户能够通过 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 OAuthProvider 创建 OAuthProvider 实例。

    Web

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

    Web

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

    Web

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

    Web

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

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

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

    Web

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

    Web

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

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

      Web

      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

      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()

    Web

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

    Web

    firebase.auth().signInWithRedirect(provider);

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

    Web

    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

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

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

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

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

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 扩展程序应用,请参阅“屏幕外文档”指南

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

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

令牌撤消

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

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

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

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

    然后,从 OAuthCredential 获取 Apple OAuth 访问令牌,并使用该令牌调用 revokeAccessToken(auth, token) 以撤消 Apple OAuth 访问令牌。

    const provider = new OAuthProvider('apple.com');
    provider.addScope('email');
    provider.addScope('name');
    
    const auth = getAuth();
    signInWithPopup(auth, provider).then(result => {
      // Get the Apple OAuth access token.
      const credential = OAuthProvider.credentialFromResult(result);
      const accessToken = credential.accessToken;
    
      // Revoke the Apple OAuth access token.
      revokeAccessToken(auth, accessToken)
        .then(() => {
          // Token revoked.
    
          // Delete the user account.
          // ...
        })
        .catch(error => {
          // An error happened.
          // ...
        });
    });
    
  3. 最后,删除用户账号(以及所有相关数据)。

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

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

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

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

      Web

      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

      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

    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

    // 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 DatabaseCloud Storage 安全规则中,您可以从 auth 变量获取已登录用户的唯一用户 ID,然后利用此 ID 来控制用户可以访问哪些数据。

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

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

Web

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

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

Web

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