如何通过Firebase PhoneAuthProvider检测短信验证码是否发送至其他设备
解决Firebase PhoneAuth无法检测用户输入他人手机号的问题
嘿,我来帮你搞定这个困扰!你遇到的核心问题是:用户输入别人的手机号后,只要拿到验证码并输入正确,Firebase的onVerificationCompleted就会触发,但你没法判断这个手机号到底是不是当前用户自己的。下面给你几个实用的解决方案,结合代码示例一步步来:
1. 区分「自动验证」和「手动验证」,对手动验证加二次确认
Firebase PhoneAuth的onVerificationCompleted会在两种场景下触发:
- 自动验证:当前设备插着该手机号的SIM卡,系统自动完成验证,不需要用户手动输入验证码(这基本能确定是用户自己的手机号)
- 手动验证:用户输入了验证码(可能是别人手机号的验证码,别人把验证码转发给了他)
我们可以通过一个标志位区分这两种场景,对手动验证的情况强制加二次确认:
private boolean isAutoVerification = true; // 发起手机号验证的核心代码 private void startPhoneVerification(String phoneNumber) { PhoneAuthProvider.getInstance().verifyPhoneNumber( phoneNumber, 60, TimeUnit.SECONDS, this, new PhoneAuthProvider.OnVerificationStateChangedCallbacks() { @Override public void onVerificationCompleted(PhoneAuthCredential credential) { if (isAutoVerification) { // 自动验证通过,大概率是当前设备的手机号,直接继续流程 Toast.makeText(RegistrationStepOne.this, "手机号验证成功!", Toast.LENGTH_SHORT).show(); navigateToNextStep(); } else { // 手动输入验证码通过,需要二次确认 showPhoneConfirmationDialog(credential.getPhoneNumber()); } } @Override public void onVerificationFailed(FirebaseException e) { Toast.makeText(RegistrationStepOne.this, "验证失败:" + e.getMessage(), Toast.LENGTH_SHORT).show(); } @Override public void onCodeSent(String verificationId, PhoneAuthProvider.ForceResendingToken token) { // 验证码已发送,用户需要手动输入,修改标志位 isAutoVerification = false; // 跳转到验证码输入页面,传递必要参数 Intent codeInputIntent = new Intent(RegistrationStepOne.this, VerifyCodeActivity.class); codeInputIntent.putExtra("verificationId", verificationId); codeInputIntent.putExtra("resendToken", token); startActivity(codeInputIntent); } }); } // 二次确认对话框 private void showPhoneConfirmationDialog(String phoneNumber) { new AlertDialog.Builder(this) .setTitle("确认手机号") .setMessage("你确定" + phoneNumber + "是你的手机号吗?我们会再次发送一条确认短信,请在你的设备上查看并完成验证。") .setPositiveButton("确认", (dialog, which) -> { // 再次发起验证,确保是当前设备操作 reVerifyPhoneNumber(phoneNumber); }) .setNegativeButton("取消", (dialog, which) -> { // 回到手机号输入页面,让用户重新输入 dialog.dismiss(); }) .show(); } // 二次验证逻辑 private void reVerifyPhoneNumber(String phoneNumber) { PhoneAuthProvider.getInstance().verifyPhoneNumber( phoneNumber, 60, TimeUnit.SECONDS, this, new PhoneAuthProvider.OnVerificationStateChangedCallbacks() { @Override public void onVerificationCompleted(PhoneAuthCredential credential) { Toast.makeText(RegistrationStepOne.this, "手机号确认成功!", Toast.LENGTH_SHORT).show(); navigateToNextStep(); } @Override public void onVerificationFailed(FirebaseException e) { Toast.makeText(RegistrationStepOne.this, "确认失败,请检查手机号是否正确", Toast.LENGTH_SHORT).show(); } @Override public void onCodeSent(String verificationId, PhoneAuthProvider.ForceResendingToken token) { // 跳转到二次验证码输入页面 Intent confirmCodeIntent = new Intent(RegistrationStepOne.this, ConfirmCodeActivity.class); confirmCodeIntent.putExtra("verificationId", verificationId); startActivity(confirmCodeIntent); } }); } // 跳转下一步的通用方法 private void navigateToNextStep() { Intent step2 = new Intent(RegistrationStepOne.this, RegistrationStepTwo.class); startActivity(step2); }
2. 尝试获取当前设备手机号做预校验(有局限性)
你可以尝试读取当前设备的SIM卡手机号,和用户输入的手机号对比,如果不一致就给出提示。注意:
- Android 10+需要
READ_PHONE_NUMBERS权限 - 部分设备(比如双卡机、无SIM卡设备)可能无法读取到手机号
private static final int REQUEST_READ_PHONE_PERMISSION = 1001; // 检查权限并获取设备手机号 private void checkDevicePhoneNumber(String userInputPhone) { if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_PHONE_NUMBERS) != PackageManager.PERMISSION_GRANTED) { ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.READ_PHONE_NUMBERS}, REQUEST_READ_PHONE_PERMISSION); return; } TelephonyManager telephonyManager = (TelephonyManager) getSystemService(Context.TELEPHONY_SERVICE); if (telephonyManager == null) { startPhoneVerification(userInputPhone); return; } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { String devicePhone = telephonyManager.getLine1Number(); if (devicePhone != null && !devicePhone.isEmpty()) { // 统一格式(比如去掉空格、国家代码差异)后对比 if (!devicePhone.replaceAll("\\s", "").equals(userInputPhone.replaceAll("\\s", ""))) { new AlertDialog.Builder(this) .setTitle("注意") .setMessage("你输入的手机号和当前设备的手机号不一致,确定要继续吗?") .setPositiveButton("继续", (dialog, which) -> startPhoneVerification(userInputPhone)) .setNegativeButton("修改", (dialog, which) -> dialog.dismiss()) .show(); return; } } } // 无法获取或手机号一致,直接发起验证 startPhoneVerification(userInputPhone); } // 权限回调 @Override public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { super.onRequestPermissionsResult(requestCode, permissions, grantResults); if (requestCode == REQUEST_READ_PHONE_PERMISSION) { if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { String userInputPhone = etPhoneNumber.getText().toString().trim(); checkDevicePhoneNumber(userInputPhone); } else { // 用户拒绝权限,直接发起验证 startPhoneVerification(etPhoneNumber.getText().toString().trim()); } } }
3. 绑定设备ID,后续登录做校验
用户首次验证成功后,把用户的UID和当前设备的唯一ID绑定到Firebase数据库(Realtime Database或Firestore),下次登录时检查设备ID是否匹配,不匹配则要求二次验证:
// 验证成功后保存设备ID到Firestore private void saveDeviceIdToFirestore(FirebaseUser user) { String deviceId = Settings.Secure.getString(getContentResolver(), Settings.Secure.ANDROID_ID); FirebaseFirestore.getInstance().collection("users") .document(user.getUid()) .set(new HashMap<String, Object>() {{ put("deviceId", deviceId); put("phoneNumber", user.getPhoneNumber()); }}, SetOptions.merge()) .addOnSuccessListener(aVoid -> {/* 保存成功 */}) .addOnFailureListener(e -> {/* 处理失败 */}); } // 登录时检查设备ID private void checkDeviceIdOnLogin(FirebaseUser user) { String currentDeviceId = Settings.Secure.getString(getContentResolver(), Settings.Secure.ANDROID_ID); FirebaseFirestore.getInstance().collection("users") .document(user.getUid()) .get() .addOnSuccessListener(doc -> { String savedDeviceId = doc.getString("deviceId"); if (!currentDeviceId.equals(savedDeviceId)) { // 设备不一致,要求二次验证 showReVerificationDialog(); } else { // 设备匹配,进入主界面 startActivity(new Intent(this, MainActivity.class)); } }); }
总结
最推荐的是第一种方案:区分自动/手动验证,对手动验证加二次确认——既不会影响正常用户的体验,又能有效拦截「输入他人手机号+获取验证码」的情况。第二种方案作为辅助预校验,第三种方案用于后续登录的安全加固。
内容的提问来源于stack exchange,提问作者MbaiMburu




