使用 Firebase Authentication,您可以向用户的手机发送短信以协助其登录,用户使用短信中包含的一次性验证码即可登录。
如需向应用中添加电话号码登录方式,最简单的方法就是使用 FirebaseUI。该库中包含一个普适性登录 widget,可为电话号码登录、基于密码登录和联合登录实现登录流程。本文档介绍了如何使用 Firebase SDK 实现电话号码登录流程。
准备工作
- 将 Firebase 添加到您的 Android 项目(如果尚未添加)。
-
在模块(应用级)Gradle 文件(通常是
<project>/<app-module>/build.gradle.kts
或<project>/<app-module>/build.gradle
)中,添加 Firebase Authentication 库的依赖项。我们建议使用 Firebase Android BoM 来实现库版本控制。dependencies { // Import the BoM for the Firebase platform implementation(platform("com.google.firebase:firebase-bom:33.6.0")) // Add the dependency for the Firebase Authentication library // When using the BoM, you don't specify versions in Firebase library dependencies implementation("com.google.firebase:firebase-auth") }
借助 Firebase Android BoM,可确保您的应用使用的始终是 Firebase Android 库的兼容版本。
(替代方法) 在不使用 BoM 的情况下添加 Firebase 库依赖项
如果您选择不使用 Firebase BoM,则必须在每个 Firebase 库的依赖项行中指定相应的库版本。
请注意,如果您在应用中使用多个 Firebase 库,我们强烈建议您使用 BoM 来管理库版本,从而确保所有版本都兼容。
dependencies { // Add the dependency for the Firebase Authentication library // When NOT using the BoM, you must specify versions in Firebase library dependencies implementation("com.google.firebase:firebase-auth:23.1.0") }
- 如果您尚未将您的应用与 Firebase 项目关联,请在 Firebase 控制台中进行关联。
- 如果您尚未在 Firebase 控制台中设置该应用的 SHA-1 哈希,请执行此操作。如需了解如何查找应用的 SHA-1 哈希,请参阅对客户端进行身份验证。
安全考量
与其他可用的方法相比,仅使用电话号码进行身份验证的方法虽然便捷,但安全性较低,因为电话号码的所有权可以很容易地在用户之间转移。此外,在具有多份用户个人资料的设备上,任何一位可以接收短信的用户都能使用该设备的电话号码登录账号。
如果您选择在应用中使用电话号码登录方法,应同时提供更安全的登录方法,并将使用电话号码登录的安全隐患告知用户。
为 Firebase 项目启用电话号码登录方法
如需让用户能够通过短信登录,您必须先为 Firebase 项目启用电话号码登录方法,步骤如下:
- 在 Firebase 控制台中,打开 Authentication 部分。
- 在登录方法页面上,启用电话号码登录方法。
启用应用验证
如需使用电话号码身份验证,Firebase 必须要能验证电话号码登录请求是否来自您的应用。Firebase Authentication 可通过以下三种方法完成此操作:
- Play Integrity API:如果用户的设备安装了 Google Play services,并且 Firebase Authentication 可以使用 Play Integrity API 验证设备的合法性,就可以继续使用电话号码登录方法。Firebase Authentication 会在 Google 拥有的项目(而不是您自己的项目)上启用 Play Integrity API;因此不会计入您项目的 Play Integrity API 配额。Play Integrity 支持 Authentication SDK v21.2.0+
(Firebase BoM v31.4.0+)。
如需使用 Play Integrity,请在 Firebase 控制台的项目设置中指定应用的 SHA-256 指纹(如果您尚未执行此操作)。如需详细了解如何获取您的应用的 SHA-256 指纹,请参阅对客户端进行身份验证。
- reCAPTCHA 验证:如果用户设备上未安装 Google Play services,或者其他原因导致无法使用 Play Integrity,Firebase Authentication 会使用 reCAPTCHA 验证来完成电话号码登录流程。reCAPTCHA 验证通常可以在不需要用户回答任何问题的情况下完成。请注意,此流程要求您的应用关联 SHA-1。此外,此流程还要求确保您的 API 密钥在
PROJECT_ID.firebaseapp.com
上不受限制或已列入许可名单。在以下情形中,系统会触发 reCAPTCHA:
- 如果最终用户的设备未安装 Google Play services 服务。
- 如果相关应用未通过 Google Play Store 分发(针对 Authentication SDK 21.2.0 及以上版本)。
- 如果获得的 SafetyNet 令牌无效(针对 21.2.0 以下的 Authentication SDK 版本)。
将 SafetyNet 或 Play Integrity 用于应用验证时,系统会用 Google Play Store 中所确定的应用名称来填充短信模板中的
%APP_NAME%
字段。在触发 reCAPTCHA 的情形下,%APP_NAME%
会填充为PROJECT_ID.firebaseapp.com
。
forceRecaptchaFlowForTesting
强制执行 reCAPTCHA 验证流程;也可以使用 setAppVerificationDisabledForTesting
停用应用验证(使用虚构电话号码时)。
问题排查
使用 reCAPTCHA 进行应用验证时出现“缺少初始状态”错误
如果 reCAPTCHA 流程成功完成,但未将用户重定向回原生应用,就可能会发生这种情况。如果发生这种情况,系统会将用户重定向到后备网址
PROJECT_ID.firebaseapp.com/__/auth/handler
。在 Firefox 浏览器中,系统默认禁止打开原生应用链接。如果您在 Firefox 上看到上述错误,请按照设置 Firefox(Android 版)打开原生应用中的链接中的步骤操作,以允许打开应用链接。
向用户的电话发送验证码
如需启动电话号码登录流程,请向用户显示一个提示其输入电话号码的界面。虽然相关的法律要求可能不尽相同,但为了避免用户不满,最佳做法是告知用户,如果他们选择使用电话号码登录方式,则可能会收到一条验证短信,并需按标准费率支付短信费用。
然后,将用户的电话号码传递给 PhoneAuthProvider.verifyPhoneNumber
方法,请求 Firebase 验证该电话号码。例如:
Kotlin+KTX
val options = PhoneAuthOptions.newBuilder(auth) .setPhoneNumber(phoneNumber) // Phone number to verify .setTimeout(60L, TimeUnit.SECONDS) // Timeout and unit .setActivity(this) // Activity (for callback binding) .setCallbacks(callbacks) // OnVerificationStateChangedCallbacks .build() PhoneAuthProvider.verifyPhoneNumber(options)
Java
PhoneAuthOptions options = PhoneAuthOptions.newBuilder(mAuth) .setPhoneNumber(phoneNumber) // Phone number to verify .setTimeout(60L, TimeUnit.SECONDS) // Timeout and unit .setActivity(this) // (optional) Activity for callback binding // If no activity is passed, reCAPTCHA verification can not be used. .setCallbacks(mCallbacks) // OnVerificationStateChangedCallbacks .build(); PhoneAuthProvider.verifyPhoneNumber(options);
verifyPhoneNumber
方法是可重入的:如果您对其进行多次调用(例如在某个 Activity 的 onStart
方法中),除非原始请求已超时,否则 verifyPhoneNumber
方法将不会发送第二条短信。
如果您的应用在用户成功登录前关闭(例如,当用户正在使用其短信应用时),则可以利用此行为继续完成电话号码登录流程。在您调用 verifyPhoneNumber
方法后,设置一个表示正在进行验证的标志。然后,在您的 Activity 的 onSaveInstanceState
方法中保存该标志,并在 onRestoreInstanceState
方法中恢复该标志。最后,在该 Activity 的 onStart
方法中检查是否已在进行验证,如果是,则再次调用 verifyPhoneNumber
方法。务必在验证完成或失败时清除该标志(请参阅验证回调函数)。
为了轻松处理屏幕旋转以及涉及 Activity 重启的其他情况,请将您的 Activity 传递给 verifyPhoneNumber
方法。当 Activity 停止时,系统将自动取消与回调函数的关联,因此您可以在回调方法中自由编写界面转换代码。
您还可以在 Auth 实例的 setLanguageCode
方法中指定身份验证语言,对 Firebase 发送的短信进行本地化。
Kotlin+KTX
auth.setLanguageCode("fr") // To apply the default app language instead of explicitly setting it. // auth.useAppLanguage()
Java
auth.setLanguageCode("fr"); // To apply the default app language instead of explicitly setting it. // auth.useAppLanguage();
在调用 PhoneAuthProvider.verifyPhoneNumber
方法时,您还必须提供一个 OnVerificationStateChangedCallbacks
的实例,该实例包含了负责处理请求结果的回调函数的实现代码。例如:
Kotlin+KTX
callbacks = object : PhoneAuthProvider.OnVerificationStateChangedCallbacks() { override fun onVerificationCompleted(credential: PhoneAuthCredential) { // This callback will be invoked in two situations: // 1 - Instant verification. In some cases the phone number can be instantly // verified without needing to send or enter a verification code. // 2 - Auto-retrieval. On some devices Google Play services can automatically // detect the incoming verification SMS and perform verification without // user action. Log.d(TAG, "onVerificationCompleted:$credential") signInWithPhoneAuthCredential(credential) } override fun onVerificationFailed(e: FirebaseException) { // This callback is invoked in an invalid request for verification is made, // for instance if the the phone number format is not valid. Log.w(TAG, "onVerificationFailed", e) if (e is FirebaseAuthInvalidCredentialsException) { // Invalid request } else if (e is FirebaseTooManyRequestsException) { // The SMS quota for the project has been exceeded } else if (e is FirebaseAuthMissingActivityForRecaptchaException) { // reCAPTCHA verification attempted with null Activity } // Show a message and update the UI } override fun onCodeSent( verificationId: String, token: PhoneAuthProvider.ForceResendingToken, ) { // The SMS verification code has been sent to the provided phone number, we // now need to ask the user to enter the code and then construct a credential // by combining the code with a verification ID. Log.d(TAG, "onCodeSent:$verificationId") // Save verification ID and resending token so we can use them later storedVerificationId = verificationId resendToken = token } }
Java
mCallbacks = new PhoneAuthProvider.OnVerificationStateChangedCallbacks() { @Override public void onVerificationCompleted(@NonNull PhoneAuthCredential credential) { // This callback will be invoked in two situations: // 1 - Instant verification. In some cases the phone number can be instantly // verified without needing to send or enter a verification code. // 2 - Auto-retrieval. On some devices Google Play services can automatically // detect the incoming verification SMS and perform verification without // user action. Log.d(TAG, "onVerificationCompleted:" + credential); signInWithPhoneAuthCredential(credential); } @Override public void onVerificationFailed(@NonNull FirebaseException e) { // This callback is invoked in an invalid request for verification is made, // for instance if the the phone number format is not valid. Log.w(TAG, "onVerificationFailed", e); if (e instanceof FirebaseAuthInvalidCredentialsException) { // Invalid request } else if (e instanceof FirebaseTooManyRequestsException) { // The SMS quota for the project has been exceeded } else if (e instanceof FirebaseAuthMissingActivityForRecaptchaException) { // reCAPTCHA verification attempted with null Activity } // Show a message and update the UI } @Override public void onCodeSent(@NonNull String verificationId, @NonNull PhoneAuthProvider.ForceResendingToken token) { // The SMS verification code has been sent to the provided phone number, we // now need to ask the user to enter the code and then construct a credential // by combining the code with a verification ID. Log.d(TAG, "onCodeSent:" + verificationId); // Save verification ID and resending token so we can use them later mVerificationId = verificationId; mResendToken = token; } };
验证回调函数
在大多数应用中,您需要实现 onVerificationCompleted
、onVerificationFailed
和 onCodeSent
回调函数。此外可能还需要实现 onCodeAutoRetrievalTimeOut
方法,具体取决于应用的要求。
onVerificationCompleted(PhoneAuthCredential)
在下列两种情况下会调用此方法:
- 即时验证:在某些情况下可以即时验证电话号码,而无需发送或输入验证码。
- 自动检索:在某些设备上,Google Play 服务可以自动检测收到的验证短信并进行验证,而无需用户执行任何操作。(某些运营商可能不支持这项功能。)此情况下会使用 SMS Retriever API,该 API 会在短信末尾添加 11 个字符的哈希。
PhoneAuthCredential
对象来让该用户登录。
onVerificationFailed(FirebaseException)
调用此方法的目的是响应无效的验证请求(例如所指定的电话号码或验证码无效的请求)。
onCodeSent(String verificationId, PhoneAuthProvider.ForceResendingToken)
可选。通过短信将验证码发送到所提供的电话号码后,可调用此方法。
当调用此方法时,大多数应用会显示一个界面,提示用户输入短信中的验证码。(同时,系统可能正在后台进行自动验证。)当用户输入验证码后,您便可使用该验证码和之前传递给该方法的验证 ID 来创建 PhoneAuthCredential
对象,然后使用该对象让用户登录。但是,有些应用可能会等到 onCodeAutoRetrievalTimeOut
被调用之后才会显示验证码界面(不推荐此做法)。
onCodeAutoRetrievalTimeOut(String verificationId)
可选。如果没有先触发 onVerificationCompleted
方法,在指定给 verifyPhoneNumber
的超时时长过去后,可调用此方法。在没有 SIM 卡的设备上,由于不能进行短信自动检索,因此系统会立即调用此方法。
有些应用在自动验证期会阻止用户输入,直到自动验证期超时后,才会显示一个界面提示用户输入短信中的验证码(不推荐此做法)。
创建 PhoneAuthCredential 对象
在用户输入 Firebase 发送至用户手机的验证码后,使用该验证码和之前传递给 onCodeSent
或 onCodeAutoRetrievalTimeOut
回调函数的验证 ID 创建一个 PhoneAuthCredential
对象。(调用 onVerificationCompleted
方法时,您将直接获得一个 PhoneAuthCredential
对象,因此可以跳过这一步骤。)
如需创建 PhoneAuthCredential
对象,请调用 PhoneAuthProvider.getCredential
:
Kotlin+KTX
val credential = PhoneAuthProvider.getCredential(verificationId!!, code)
Java
PhoneAuthCredential credential = PhoneAuthProvider.getCredential(verificationId, code);
登录用户
在您获取了 PhoneAuthCredential
对象(无论是在 onVerificationCompleted
回调函数中还是通过调用 PhoneAuthProvider.getCredential
方法获取)后,将 PhoneAuthCredential
对象传递给 FirebaseAuth.signInWithCredential
方法以完成登录流程:
Kotlin+KTX
private fun signInWithPhoneAuthCredential(credential: PhoneAuthCredential) { auth.signInWithCredential(credential) .addOnCompleteListener(this) { task -> if (task.isSuccessful) { // Sign in success, update UI with the signed-in user's information Log.d(TAG, "signInWithCredential:success") val user = task.result?.user } else { // Sign in failed, display a message and update the UI Log.w(TAG, "signInWithCredential:failure", task.exception) if (task.exception is FirebaseAuthInvalidCredentialsException) { // The verification code entered was invalid } // Update UI } } }
Java
private void signInWithPhoneAuthCredential(PhoneAuthCredential credential) { mAuth.signInWithCredential(credential) .addOnCompleteListener(this, new OnCompleteListener<AuthResult>() { @Override public void onComplete(@NonNull Task<AuthResult> task) { if (task.isSuccessful()) { // Sign in success, update UI with the signed-in user's information Log.d(TAG, "signInWithCredential:success"); FirebaseUser user = task.getResult().getUser(); // Update UI } else { // Sign in failed, display a message and update the UI Log.w(TAG, "signInWithCredential:failure", task.getException()); if (task.getException() instanceof FirebaseAuthInvalidCredentialsException) { // The verification code entered was invalid } } } }); }
使用虚构的手机号码进行测试
您可以通过 Firebase 控制台设置虚构电话号码以用于开发。使用虚构的电话号码进行测试具有以下优势:
- 测试电话号码身份验证时不会占用使用量配额。
- 测试电话号码身份验证时无需发送实际的短信。
- 可使用同一电话号码运行连续测试,而不会受到短信发送数量的限制。如果审核人员恰好使用相同的电话号码进行测试,这项优势会使在 App Store 审核过程中遭拒的风险降至最低。
- 无需其他操作即可在开发环境中轻松进行测试,例如可以在 iOS 模拟器或没有 Google Play 服务的 Android 模拟器中进行开发。
- 可编写集成测试,而不会被通常在生产环境中应用于真实电话号码的安全检查所屏蔽。
虚构的电话号码必须满足以下要求:
- 确保使用确属虚构且不存在的电话号码。Firebase Authentication 不允许您将真实用户使用的现有电话号码设为测试号码。您可以使用以 555 为前缀的数字作为美国测试电话号码,例如 +1 650-555-3434
- 电话号码必须采用正确的格式,以符合长度要求和其他限制。这些电话号码仍将与真实用户的电话号码一样经过相同的验证。
- 您最多可以添加 10 个电话号码用于开发。
- 使用难以猜到的测试电话号码/验证码,并经常更换。
创建虚构的电话号码和验证码
- 在 Firebase 控制台中,打开 Authentication 部分。
- 在 Sign in method(登录方法)标签页中,启用电话号码提供方(如果您尚未启用)。
- 打开用于测试的电话号码折叠菜单。
- 提供您想要用于测试的电话号码,例如 +1 650-555-3434。
- 为该特定号码提供 6 位验证码,例如 654321。
- 添加该号码。如有需要,您可以删除电话号码及其验证码,只需将鼠标悬停在相应的行上并点击垃圾桶图标即可。
手动测试
您可以直接在应用中开始使用虚构的电话号码。这样一来,您就可以在开发阶段执行手动测试,而不会遇到配额问题或受到限制。您也可以直接通过 iOS 模拟器或未安装 Google Play 服务的 Android 模拟器进行测试。
当您提供虚构电话号码并发送验证码时,系统实际上不会发送短信。作为替代,您需要提供事先配置的验证码来完成登录。
完成登录后,系统会使用该电话号码创建一位 Firebase 用户。该用户的行为和属性与真实电话号码用户相同,并且可通过同样的方式使用 Realtime Database/Cloud Firestore 和其他服务。在此过程中生成的 ID 令牌与真实电话号码用户的令牌具有相同的签名。
您还可以利用自定义声明为此类用户设置测试角色,以将其作为虚构用户区分开来(如果您想进一步限制其访问权限)。
如需手动触发 reCAPTCHA 流程以进行测试,请使用 forceRecaptchaFlowForTesting()
方法。
// Force reCAPTCHA flow FirebaseAuth.getInstance().getFirebaseAuthSettings().forceRecaptchaFlowForTesting();
集成测试
除了手动测试外,Firebase Authentication 还提供了 API,帮助您编写用于进行电话号码身份验证测试的集成测试。这些 API 通过停用 reCAPTCHA 要求(在 Web 应用中)和静默推送通知(在 iOS 应用中)来停用应用验证。因此,您可以在这些流程中进行自动测试,并且实现起来更加容易。此外,借助这些 API,您还可以在 Android 上测试即时验证流程。
在 Android 设备上,先调用 setAppVerificationDisabledForTesting()
,然后再调用 signInWithPhoneNumber
。这样会自动停用应用验证,因此您无需手动解析即可传递电话号码。即使 Play Integrity 和 reCAPTCHA 都已停用,使用真实的电话号码也无法完成登录。此 API 只能使用虚构的电话号码。
// Turn off phone auth app verification. FirebaseAuth.getInstance().getFirebaseAuthSettings() .setAppVerificationDisabledForTesting();
使用虚构号码调用 verifyPhoneNumber
会触发 onCodeSent
回调函数,您将需要向其提供对应的验证码。这样,您将可以在 Android 模拟器中进行测试。
Java
String phoneNum = "+16505554567"; String testVerificationCode = "123456"; // Whenever verification is triggered with the whitelisted number, // provided it is not set for auto-retrieval, onCodeSent will be triggered. FirebaseAuth auth = FirebaseAuth.getInstance(); PhoneAuthOptions options = PhoneAuthOptions.newBuilder(auth) .setPhoneNumber(phoneNum) .setTimeout(60L, TimeUnit.SECONDS) .setActivity(this) .setCallbacks(new PhoneAuthProvider.OnVerificationStateChangedCallbacks() { @Override public void onCodeSent(@NonNull String verificationId, @NonNull PhoneAuthProvider.ForceResendingToken forceResendingToken) { // Save the verification id somewhere // ... // The corresponding whitelisted code above should be used to complete sign-in. MainActivity.this.enableUserManuallyInputCode(); } @Override public void onVerificationCompleted(@NonNull PhoneAuthCredential phoneAuthCredential) { // Sign in with the credential // ... } @Override public void onVerificationFailed(@NonNull FirebaseException e) { // ... } }) .build(); PhoneAuthProvider.verifyPhoneNumber(options);
Kotlin+KTX
val phoneNum = "+16505554567" val testVerificationCode = "123456" // Whenever verification is triggered with the whitelisted number, // provided it is not set for auto-retrieval, onCodeSent will be triggered. val options = PhoneAuthOptions.newBuilder(Firebase.auth) .setPhoneNumber(phoneNum) .setTimeout(30L, TimeUnit.SECONDS) .setActivity(this) .setCallbacks(object : PhoneAuthProvider.OnVerificationStateChangedCallbacks() { override fun onCodeSent( verificationId: String, forceResendingToken: PhoneAuthProvider.ForceResendingToken, ) { // Save the verification id somewhere // ... // The corresponding whitelisted code above should be used to complete sign-in. this@MainActivity.enableUserManuallyInputCode() } override fun onVerificationCompleted(phoneAuthCredential: PhoneAuthCredential) { // Sign in with the credential // ... } override fun onVerificationFailed(e: FirebaseException) { // ... } }) .build() PhoneAuthProvider.verifyPhoneNumber(options)
此外,您可以在 Android 中测试自动检索流程,只需调用 setAutoRetrievedSmsCodeForPhoneNumber
方法,为自动检索设置虚构号码及其对应的验证码即可。
调用 verifyPhoneNumber
方法时,它会直接使用 PhoneAuthCredential
触发 onVerificationCompleted
方法。这仅适用于虚构的电话号码。
将您的应用发布到 Google Play 商店时,请确保此验证已停用,并且没有将虚构电话号码硬编码到您的应用中。
Java
// The test phone number and code should be whitelisted in the console. String phoneNumber = "+16505554567"; String smsCode = "123456"; FirebaseAuth firebaseAuth = FirebaseAuth.getInstance(); FirebaseAuthSettings firebaseAuthSettings = firebaseAuth.getFirebaseAuthSettings(); // Configure faking the auto-retrieval with the whitelisted numbers. firebaseAuthSettings.setAutoRetrievedSmsCodeForPhoneNumber(phoneNumber, smsCode); PhoneAuthOptions options = PhoneAuthOptions.newBuilder(firebaseAuth) .setPhoneNumber(phoneNumber) .setTimeout(60L, TimeUnit.SECONDS) .setActivity(this) .setCallbacks(new PhoneAuthProvider.OnVerificationStateChangedCallbacks() { @Override public void onVerificationCompleted(@NonNull PhoneAuthCredential credential) { // Instant verification is applied and a credential is directly returned. // ... } // ... }) .build(); PhoneAuthProvider.verifyPhoneNumber(options);
Kotlin+KTX
// The test phone number and code should be whitelisted in the console. val phoneNumber = "+16505554567" val smsCode = "123456" val firebaseAuth = Firebase.auth val firebaseAuthSettings = firebaseAuth.firebaseAuthSettings // Configure faking the auto-retrieval with the whitelisted numbers. firebaseAuthSettings.setAutoRetrievedSmsCodeForPhoneNumber(phoneNumber, smsCode) val options = PhoneAuthOptions.newBuilder(firebaseAuth) .setPhoneNumber(phoneNumber) .setTimeout(60L, TimeUnit.SECONDS) .setActivity(this) .setCallbacks(object : PhoneAuthProvider.OnVerificationStateChangedCallbacks() { override fun onVerificationCompleted(credential: PhoneAuthCredential) { // Instant verification is applied and a credential is directly returned. // ... } // ... }) .build() PhoneAuthProvider.verifyPhoneNumber(options)
后续步骤
在用户首次登录后,系统会创建一个新的用户账号,并将其与该用户登录时使用的凭据(即用户名和密码、电话号码或者身份验证提供方信息)相关联。此新账号存储在您的 Firebase 项目中,无论用户采用何种方式登录,您项目中的每个应用都可以使用此账号来识别用户。
-
在您的应用中,您可以从
FirebaseUser
对象获取用户的基本个人资料信息。请参阅管理用户。 在您的 Firebase Realtime Database 和 Cloud Storage 安全规则中,您可以从
auth
变量获取已登录用户的唯一用户 ID,然后利用此 ID 来控制用户可以访问哪些数据。
您可以通过将身份验证提供方凭据关联至现有用户账号,让用户可以使用多个身份验证提供方登录您的应用。
如需将用户退出登录,请调用 signOut
:
Kotlin+KTX
Firebase.auth.signOut()
Java
FirebaseAuth.getInstance().signOut();