You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

如何通过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

火山引擎 最新活动