您可以使用 Firebase SDK 执行端到端 OAuth 2.0 登录流程,让您的用户可使用自己的 Apple ID 进行 Firebase 身份验证。
准备工作
如需让用户能够通过 Apple 账号登录,首先需在 Apple 的开发者网站上配置“通过 Apple 登录”功能,然后启用 Apple 作为您的 Firebase 项目的登录服务提供方。
加入 Apple Developer Program
“通过 Apple 登录”功能只能由 Apple Developer Program 的成员配置。
配置“通过 Apple 登录”
在 Apple Developer 网站上,执行以下操作:
-
按照为网页配置“通过 Apple 登录”的第一部分所述,将您的网站关联至您的应用。出现提示时,将以下网址注册为返回网址:
https://YOUR_FIREBASE_PROJECT_ID.firebaseapp.com/__/auth/handler
您可以通过 Firebase 控制台设置页面获取您的 Firebase 项目 ID。
完成后,请记下新的服务 ID,下一部分需要用到该 ID。
- 创建“通过 Apple 登录”的私钥。下一部分需要用到您的新私钥和密钥 ID。
-
如果您会使用任何将向用户发送电子邮件的 Firebase Authentication 功能,包括电子邮件链接登录、电子邮件地址验证、账号更改撤消等功能,请配置 Apple 私人电子邮件中继服务并注册
noreply@YOUR_FIREBASE_PROJECT_ID.firebaseapp.com
(或您的自定义电子邮件模板网域),以便 Apple 可以将 Firebase Authentication 所发送的电子邮件中继到匿名化的 Apple 电子邮件地址。
启用 Apple 作为登录提供方
- 将 Firebase 添加到您的 Android 项目。当您在 Firebase 控制台中设置应用时,请务必注册应用的 SHA-1 签名。
- 在 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 处理登录流程
在 Android 设备上,想要通过用户的 Apple 账号对其进行 Firebase 身份验证,最简单的方法是使用 Firebase Android SDK 来处理整个登录流程。
如需使用 Firebase Android SDK 处理登录流程,请按以下步骤操作:
使用提供方 ID
apple.com
通过其构建器构建OAuthProvider
实例:Kotlin+KTX
val provider = OAuthProvider.newBuilder("apple.com")
Java
OAuthProvider.Builder provider = OAuthProvider.newBuilder("apple.com");
可选:指定您希望向身份验证提供方申请的超出默认范围的额外 OAuth 2.0 范围。
Kotlin+KTX
provider.setScopes(arrayOf("email", "name"))
Java
List<String> scopes = new ArrayList<String>() { { add("email"); add("name"); } }; provider.setScopes(scopes);
默认情况下,当启用每个电子邮件地址一个账号时,Firebase 会申请电子邮件和名称范围。如果您将此设置更改为每个电子邮件地址多个账号,那么除非您自行指定,否则 Firebase 不会向 Apple 申请任何范围。
可选:如果想要以英文以外的语言显示 Apple 的登录屏幕,请设置
locale
参数。如需了解受支持的语言区域,请查看“通过 Apple 登录”文档。Kotlin+KTX
// Localize the Apple authentication screen in French. provider.addCustomParameter("locale", "fr")
Java
// Localize the Apple authentication screen in French. provider.addCustomParameter("locale", "fr");
使用 OAuth 提供方对象进行 Firebase 身份验证。请注意,与其他
FirebaseAuth
操作不同,此操作会打开自定义 Chrome 标签页,从而控制您的界面。因此,请不要在您附加的OnSuccessListener
和OnFailureListener
中引用您的 Activity,因为当操作启动界面时,它们会立即分离。您应该首先检查是否已收到响应。通过这种方法登录时,您的 Activity 将位于后台,这意味着系统可以在登录流程中将其回收。为了确保发生这种情况时不让用户进行重试,您应该检查结果是否已经存在。
如需检查是否存在待处理的结果,请调用
getPendingAuthResult()
:Kotlin+KTX
val pending = auth.pendingAuthResult if (pending != null) { pending.addOnSuccessListener { authResult -> Log.d(TAG, "checkPending:onSuccess:$authResult") // Get the user profile with authResult.getUser() and // authResult.getAdditionalUserInfo(), and the ID // token from Apple with authResult.getCredential(). }.addOnFailureListener { e -> Log.w(TAG, "checkPending:onFailure", e) } } else { Log.d(TAG, "pending: null") }
Java
mAuth = FirebaseAuth.getInstance(); Task<AuthResult> pending = mAuth.getPendingAuthResult(); if (pending != null) { pending.addOnSuccessListener(new OnSuccessListener<AuthResult>() { @Override public void onSuccess(AuthResult authResult) { Log.d(TAG, "checkPending:onSuccess:" + authResult); // Get the user profile with authResult.getUser() and // authResult.getAdditionalUserInfo(), and the ID // token from Apple with authResult.getCredential(). } }).addOnFailureListener(new OnFailureListener() { @Override public void onFailure(@NonNull Exception e) { Log.w(TAG, "checkPending:onFailure", e); } }); } else { Log.d(TAG, "pending: null"); }
如果不存在待处理的结果,请通过调用
startActivityForSignInWithProvider()
来启动登录流程:Kotlin+KTX
auth.startActivityForSignInWithProvider(this, provider.build()) .addOnSuccessListener { authResult -> // Sign-in successful! Log.d(TAG, "activitySignIn:onSuccess:${authResult.user}") val user = authResult.user // ... } .addOnFailureListener { e -> Log.w(TAG, "activitySignIn:onFailure", e) }
Java
mAuth.startActivityForSignInWithProvider(this, provider.build()) .addOnSuccessListener( new OnSuccessListener<AuthResult>() { @Override public void onSuccess(AuthResult authResult) { // Sign-in successful! Log.d(TAG, "activitySignIn:onSuccess:" + authResult.getUser()); FirebaseUser user = authResult.getUser(); // ... } }) .addOnFailureListener( new OnFailureListener() { @Override public void onFailure(@NonNull Exception e) { Log.w(TAG, "activitySignIn:onFailure", e); } });
与 Firebase Authentication 支持的其他提供方不同,Apple 不提供照片网址。
此外,当用户选择不与应用共享其电子邮件时,Apple 会为该用户分配一个格式为
xyz@privaterelay.appleid.com
的唯一电子邮件地址并与应用共享该地址。如果您配置了私人电子邮件中继服务,则 Apple 会将发送到匿名化地址的电子邮件转发到用户的真实电子邮件地址。Apple 只会在用户首次登录时与应用共享用户信息(例如显示名)。Firebase 通常会在用户首次使用 Apple 账号登录时存储显示名,您可以使用
getCurrentUser().getDisplayName()
获取该显示名。不过,如果您过去曾在未使用 Firebase 的情况下通过 Apple 账号将用户登录到应用,则 Apple 不会向 Firebase 提供该用户的显示名。
重新进行身份验证和账号关联
上述模式同样适用于 startActivityForReauthenticateWithProvider()
,对于要求用户必须在近期内登录过才能执行的敏感操作,您可使用它来检索新的凭据。
Kotlin+KTX
// The user is already signed-in.
val firebaseUser = auth.getCurrentUser()
firebaseUser
.startActivityForReauthenticateWithProvider(/* activity= */ this, provider.build())
.addOnSuccessListener( authResult -> {
// User is re-authenticated with fresh tokens and
// should be able to perform sensitive operations
// like account deletion and email or password
// update.
})
.addOnFailureListener( e -> {
// Handle failure.
})
Java
// The user is already signed-in.
FirebaseUser firebaseUser = mAuth.getCurrentUser();
firebaseUser
.startActivityForReauthenticateWithProvider(/* activity= */ this, provider.build())
.addOnSuccessListener(
new OnSuccessListener<AuthResult>() {
@Override
public void onSuccess(AuthResult authResult) {
// User is re-authenticated with fresh tokens and
// should be able to perform sensitive operations
// like account deletion and email or password
// update.
}
})
.addOnFailureListener(
new OnFailureListener() {
@Override
public void onFailure(@NonNull Exception e) {
// Handle failure.
}
});
此外,您可以使用 linkWithCredential()
将不同的身份提供方关联至现有账号。
请注意,在将用户的 Apple 账号关联至其他数据之前,Apple 要求您征得用户的明确同意。
例如,若要将 Facebook 账号关联至当前的 Firebase 账号,请使用将用户登录到 Facebook 时获得的访问令牌:
Kotlin+KTX
// Initialize a Facebook credential with a Facebook access token.
val credential = FacebookAuthProvider.getCredential(token.getToken())
// Assuming the current user is an Apple user linking a Facebook provider.
mAuth.getCurrentUser().linkWithCredential(credential)
.addOnCompleteListener(this, task -> {
if (task.isSuccessful()) {
// Facebook credential is linked to the current Apple user.
// The user can now sign in to the same account
// with either Apple or Facebook.
}
});
Java
// Initialize a Facebook credential with a Facebook access token.
AuthCredential credential = FacebookAuthProvider.getCredential(token.getToken());
// Assuming the current user is an Apple user linking a Facebook provider.
mAuth.getCurrentUser().linkWithCredential(credential)
.addOnCompleteListener(this, new OnCompleteListener<AuthResult>() {
@Override
public void onComplete(@NonNull Task<AuthResult> task) {
if (task.isSuccessful()) {
// Facebook credential is linked to the current Apple user.
// The user can now sign in to the same account
// with either Apple or Facebook.
}
}
});
高级:手动处理登录流程
您还可以使用 Apple 账号进行 Firebase 身份验证,方法是使用 Apple Sign-In JS SDK 手动构建 OAuth 流程来处理登录流程,或使用 OAuth 库(如 AppAuth)来处理登录流程。
为每个登录请求生成随机字符串 (Nonce),用来确保您获取的 ID 令牌专门用于响应您应用的身份验证请求。此步骤对于防止重放攻击至关重要。
您可以在 Android 上使用
SecureRandom
生成加密的安全随机数,如以下示例所示:Kotlin+KTX
private fun generateNonce(length: Int): String { val generator = SecureRandom() val charsetDecoder = StandardCharsets.US_ASCII.newDecoder() charsetDecoder.onUnmappableCharacter(CodingErrorAction.IGNORE) charsetDecoder.onMalformedInput(CodingErrorAction.IGNORE) val bytes = ByteArray(length) val inBuffer = ByteBuffer.wrap(bytes) val outBuffer = CharBuffer.allocate(length) while (outBuffer.hasRemaining()) { generator.nextBytes(bytes) inBuffer.rewind() charsetDecoder.reset() charsetDecoder.decode(inBuffer, outBuffer, false) } outBuffer.flip() return outBuffer.toString() }
Java
private String generateNonce(int length) { SecureRandom generator = new SecureRandom(); CharsetDecoder charsetDecoder = StandardCharsets.US_ASCII.newDecoder(); charsetDecoder.onUnmappableCharacter(CodingErrorAction.IGNORE); charsetDecoder.onMalformedInput(CodingErrorAction.IGNORE); byte[] bytes = new byte[length]; ByteBuffer inBuffer = ByteBuffer.wrap(bytes); CharBuffer outBuffer = CharBuffer.allocate(length); while (outBuffer.hasRemaining()) { generator.nextBytes(bytes); inBuffer.rewind(); charsetDecoder.reset(); charsetDecoder.decode(inBuffer, outBuffer, false); } outBuffer.flip(); return outBuffer.toString(); }
然后,以十六进制字符串的形式获取随机数的 SHA246 哈希值:
Kotlin+KTX
private fun sha256(s: String): String { val md = MessageDigest.getInstance("SHA-256") val digest = md.digest(s.toByteArray()) val hash = StringBuilder() for (c in digest) { hash.append(String.format("%02x", c)) } return hash.toString() }
Java
private String sha256(String s) throws NoSuchAlgorithmException { MessageDigest md = MessageDigest.getInstance("SHA-256"); byte[] digest = md.digest(s.getBytes()); StringBuilder hash = new StringBuilder(); for (byte c: digest) { hash.append(String.format("%02x", c)); } return hash.toString(); }
您将通过登录请求发送随机数的 SHA256 哈希值,Apple 将在响应中原封不动传递该值。Firebase 通过对原始 Nonce 进行哈希处理并将其与 Apple 传递的值进行比较,来验证响应。
使用您的 OAuth 库或其他方法启动 Apple 的登录流程。请务必将经过哈希处理的随机数作为参数添加到请求中。
收到 Apple 的响应后,从该响应中获取 ID 令牌,并使用它和未经过哈希处理的随机数创建
AuthCredential
:Kotlin+KTX
val credential = OAuthProvider.newCredentialBuilder("apple.com") .setIdTokenWithRawNonce(appleIdToken, rawUnhashedNonce) .build()
Java
AuthCredential credential = OAuthProvider.newCredentialBuilder("apple.com") .setIdTokenWithRawNonce(appleIdToken, rawUnhashedNonce) .build();
使用 Firebase 凭据进行 Firebase 身份验证:
Kotlin+KTX
auth.signInWithCredential(credential) .addOnCompleteListener(this) { task -> if (task.isSuccessful) { // User successfully signed in with Apple ID token. // ... } }
Java
mAuth.signInWithCredential(credential) .addOnCompleteListener(this, new OnCompleteListener<AuthResult>() { @Override public void onComplete(@NonNull Task<AuthResult> task) { if (task.isSuccessful()) { // User successfully signed in with Apple ID token. // ... } } });
如果对 signInWithCredential
的调用成功,您可以使用 getCurrentUser
方法获取用户的账号数据。
令牌撤消
Apple 要求支持创建账号的应用必须允许用户在应用内删除其账号,如 App Store 审核指南中所述。
此外,支持“使用 Apple 账号登录”的应用应使用“使用 Apple 账号登录”REST API 来撤消用户令牌。
如需满足此要求,请执行以下步骤:
借助
startActivityForSignInWithProvider()
方法使用 Apple 账号登录并获取AuthResult
。获取 Apple 提供方的访问令牌。
Kotlin+KTX
val oauthCredential: OAuthCredential = authResult.credential val accessToken = oauthCredential.accessToken
Java
OAuthCredential oauthCredential = (OAuthCredential) authResult.getCredential(); String accessToken = oauthCredential.getAccessToken();
使用
revokeAccessToken
API 撤消令牌。Kotlin+KTX
mAuth.revokeAccessToken(accessToken) .addOnCompleteListener(this) { task -> if (task.isSuccessful) { // Access token successfully revoked // for the user ... } }
Java
mAuth.revokeAccessToken(accessToken) .addOnCompleteListener(this, new OnCompleteListener<Void>() { @Override public void onComplete(@NonNull Task<Void> task) { if (task.isSuccessful()) { // Access token successfully revoked // for the user ... } } });
- 最后,删除用户账号(以及所有相关数据)
后续步骤
在用户首次登录后,系统会创建一个新的用户账号,并将其与该用户登录时使用的凭据(即用户名和密码、电话号码或者身份验证提供方信息)相关联。此新账号存储在您的 Firebase 项目中,无论用户采用何种方式登录,您项目中的每个应用都可以使用此账号来识别用户。
-
在您的应用中,您可以从
FirebaseUser
对象获取用户的基本个人资料信息。请参阅管理用户。 在您的 Firebase Realtime Database 和 Cloud Storage 安全规则中,您可以从
auth
变量获取已登录用户的唯一用户 ID,然后利用此 ID 来控制用户可以访问哪些数据。
您可以通过将身份验证提供方凭据关联至现有用户账号,让用户可以使用多个身份验证提供方登录您的应用。
如需将用户退出登录,请调用
signOut
:Kotlin+KTX
Firebase.auth.signOut()
Java
FirebaseAuth.getInstance().signOut();
-