You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Android:如何基于Firebase实现邮箱/手机号的OTP验证

实现邮箱与手机号双渠道OTP验证

核心思路

先判断用户输入内容是邮箱还是手机号,分别触发对应OTP发送逻辑:手机号沿用你已实现的Firebase PhoneAuth流程;邮箱则需要自定义OTP生成、临时存储与邮件发送逻辑,最后统一在OTP页面完成验证并创建用户。


步骤1:添加输入类型判断工具方法

在SignUp类中新增两个辅助方法,用于识别输入内容的类型:

// 验证邮箱格式
private fun isValidEmail(email: String): Boolean {
    val emailRegex = "^[A-Za-z0-9+_.-]+@[A-Za-z0-9.-]+$"
    return email.matches(emailRegex.toRegex())
}

// 验证手机号格式(可根据目标地区调整正则)
private fun isValidPhoneNumber(phone: String): Boolean {
    val phoneRegex = "^\\+[0-9]{10,15}$" // 匹配带国际区号的手机号,如+8613xxxxxxxxx
    return phone.matches(phoneRegex.toRegex())
}

步骤2:修改注册按钮点击逻辑

更新binding.btnSignup.setOnClickListener中的代码,根据输入类型分支处理:

binding.btnSignup.setOnClickListener {
    val name = binding.name.text.toString().trim()
    val emailPh = binding.emailPhone.text.toString().trim()
    val relation = binding.relation.text.toString().trim()
    val password = binding.passwordSignup.text.toString().trim()

    // 检查必填字段是否为空
    if (name.isEmpty() || emailPh.isEmpty() || relation.isEmpty() || password.isEmpty()) {
        Toast.makeText(this, "请填写所有必填字段", Toast.LENGTH_SHORT).show()
        return@setOnClickListener
    }

    when {
        isValidEmail(emailPh) -> {
            // 触发邮箱OTP发送逻辑
            sendEmailOtp(emailPh, name, relation, password)
        }
        isValidPhoneNumber(emailPh) -> {
            // 沿用原有手机号OTP逻辑
            val options = PhoneAuthOptions.newBuilder(firebaseAuth)
                .setPhoneNumber(emailPh)
                .setTimeout(60L, TimeUnit.SECONDS)
                .setActivity(this)
                .setCallbacks(phoneAuthCallbacks) // 改名避免与邮箱回调冲突
                .build()
            PhoneAuthProvider.verifyPhoneNumber(options)
        }
        else -> {
            Toast.makeText(this, "请输入有效的邮箱或手机号", Toast.LENGTH_SHORT).show()
        }
    }
}

步骤3:实现邮箱OTP发送逻辑

邮箱OTP需要自行生成随机验证码,临时存储到Firebase Realtime Database(后续验证用),再通过邮件发送给用户:

import com.google.firebase.database.FirebaseDatabase
import kotlin.random.Random

private fun sendEmailOtp(email: String, name: String, relation: String, password: String) {
    // 生成6位随机OTP
    val otp = Random.nextInt(100000, 999999).toString()
    // 存储OTP到Realtime Database,设置5分钟过期
    val dbRef = FirebaseDatabase.getInstance().getReference("temp_otps").child(email)
    dbRef.setValue(mapOf(
        "otp" to otp,
        "expiry_time" to System.currentTimeMillis() + 300000, // 5分钟后过期
        "user_data" to mapOf(
            "name" to name,
            "relation" to relation,
            "password" to password,
            "email" to email
        )
    )).addOnSuccessListener {
        // 发送OTP邮件,建议用Firebase Cloud Functions或第三方邮件服务(如SendGrid)实现
        sendOtpEmail(email, otp)
        // 跳转到OTP输入页面,传递验证类型与目标邮箱
        val intent = Intent(this, OTP::class.java)
        intent.putExtra("VERIFICATION_TYPE", "EMAIL")
        intent.putExtra("TARGET", email)
        intent.putExtra("USER_NAME", name)
        intent.putExtra("USER_RELATION", relation)
        intent.putExtra("USER_PASSWORD", password)
        startActivity(intent)
    }.addOnFailureListener {
        Toast.makeText(this, "发送OTP失败,请重试", Toast.LENGTH_SHORT).show()
    }
}

// 邮件发送逻辑(示例,需后端实现避免泄露密钥)
private fun sendOtpEmail(email: String, otp: String) {
    // 此处调用后端接口或Cloud Functions触发邮件发送,传入邮箱与OTP
}

步骤4:调整手机号Auth回调

将原callbacks改名为phoneAuthCallbacks,并在onCodeSent中传递验证类型与用户信息:

private val phoneAuthCallbacks = object : PhoneAuthProvider.OnVerificationStateChangedCallbacks() {
    override fun onVerificationCompleted(credential: PhoneAuthCredential) {
        signInWithPhoneAuthCredential(credential)
    }

    override fun onVerificationFailed(e: FirebaseException) {
        Log.w(TAG, "手机号验证失败", e)
        Toast.makeText(this@SignUp, "手机号验证失败,请检查格式或重试", Toast.LENGTH_SHORT).show()
    }

    override fun onCodeSent(verificationId: String, token: PhoneAuthProvider.ForceResendingToken) {
        val intent = Intent(this@SignUp, OTP::class.java)
        intent.putExtra("VERIFICATION_TYPE", "PHONE")
        intent.putExtra("VERIFICATION_ID", verificationId)
        intent.putExtra("TARGET", binding.emailPhone.text.toString().trim())
        intent.putExtra("USER_NAME", binding.name.text.toString().trim())
        intent.putExtra("USER_RELATION", binding.relation.text.toString().trim())
        intent.putExtra("USER_PASSWORD", binding.passwordSignup.text.toString().trim())
        startActivity(intent)
    }
}

步骤5:修改OTP页面处理双渠道验证

在OTP页面中,根据传递的VERIFICATION_TYPE分别处理邮箱和手机号的OTP验证:

class OTP : AppCompatActivity() {
    private lateinit var firebaseAuth: FirebaseAuth
    private lateinit var dbRef: DatabaseReference

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        // 初始化绑定与Auth实例
        firebaseAuth = FirebaseAuth.getInstance()
        dbRef = FirebaseDatabase.getInstance().getReference("temp_otps")

        val verificationType = intent.getStringExtra("VERIFICATION_TYPE") ?: ""
        val target = intent.getStringExtra("TARGET") ?: ""
        val userName = intent.getStringExtra("USER_NAME") ?: ""
        val userRelation = intent.getStringExtra("USER_RELATION") ?: ""
        val userPassword = intent.getStringExtra("USER_PASSWORD") ?: ""

        // 验证按钮点击事件
        binding.btnVerify.setOnClickListener {
            val inputOtp = binding.etOtp.text.toString().trim()
            if (inputOtp.isEmpty()) {
                Toast.makeText(this, "请输入验证码", Toast.LENGTH_SHORT).show()
                return@setOnClickListener
            }

            when (verificationType) {
                "PHONE" -> {
                    val verificationId = intent.getStringExtra("VERIFICATION_ID") ?: ""
                    val credential = PhoneAuthProvider.getCredential(verificationId, inputOtp)
                    verifyPhoneCredential(credential, userName, userRelation, userPassword, target)
                }
                "EMAIL" -> {
                    verifyEmailOtp(target, inputOtp, userName, userRelation, userPassword)
                }
            }
        }
    }

    // 手机号验证完成后创建用户
    private fun verifyPhoneCredential(credential: PhoneAuthCredential, name: String, relation: String, password: String, phone: String) {
        firebaseAuth.signInWithCredential(credential).addOnSuccessListener {
            // 更新密码并完善用户信息
            firebaseAuth.currentUser?.updatePassword(password)?.addOnSuccessListener {
                val user = firebaseAuth.currentUser
                user?.updateProfile(UserProfileChangeRequest.Builder().setDisplayName(name).build())
                // 保存额外信息到数据库
                saveUserInfo(user?.uid, name, relation, phone, "")
                Toast.makeText(this, "注册成功", Toast.LENGTH_SHORT).show()
                startActivity(Intent(this, Login::class.java))
                finish()
            }
        }.addOnFailureListener {
            Toast.makeText(this, "验证码错误", Toast.LENGTH_SHORT).show()
        }
    }

    // 邮箱OTP验证
    private fun verifyEmailOtp(email: String, inputOtp: String, name: String, relation: String, password: String) {
        dbRef.child(email).get().addOnSuccessListener { snapshot ->
            if (snapshot.exists()) {
                val storedOtp = snapshot.child("otp").value.toString()
                val expiryTime = snapshot.child("expiry_time").value as Long
                if (System.currentTimeMillis() > expiryTime) {
                    Toast.makeText(this, "验证码已过期", Toast.LENGTH_SHORT).show()
                    return@addOnSuccessListener
                }
                if (storedOtp == inputOtp) {
                    // 验证成功,创建用户
                    firebaseAuth.createUserWithEmailAndPassword(email, password).addOnSuccessListener {
                        val user = firebaseAuth.currentUser
                        user?.updateProfile(UserProfileChangeRequest.Builder().setDisplayName(name).build())
                        saveUserInfo(user?.uid, name, relation, "", email)
                        // 删除临时OTP数据
                        dbRef.child(email).removeValue()
                        Toast.makeText(this, "注册成功", Toast.LENGTH_SHORT).show()
                        startActivity(Intent(this, Login::class.java))
                        finish()
                    }.addOnFailureListener {
                        Toast.makeText(this, "创建用户失败:${it.message}", Toast.LENGTH_SHORT).show()
                    }
                } else {
                    Toast.makeText(this, "验证码错误", Toast.LENGTH_SHORT).show()
                }
            } else {
                Toast.makeText(this, "验证码不存在,请重新获取", Toast.LENGTH_SHORT).show()
            }
        }
    }

    // 保存用户额外信息到数据库
    private fun saveUserInfo(uid: String?, name: String, relation: String, phone: String, email: String) {
        uid ?: return
        val userRef = FirebaseDatabase.getInstance().getReference("users").child(uid)
        userRef.setValue(mapOf(
            "name" to name,
            "relation" to relation,
            "phone" to phone,
            "email" to email
        ))
    }
}

注意事项

  • 邮箱OTP的邮件发送需通过后端实现,避免在客户端泄露邮箱服务密钥。
  • 临时存储的OTP要设置过期时间,验证成功后及时删除,减少数据冗余。
  • 手机号格式验证可根据目标地区调整正则,比如国内手机号可改为^1[3-9]\\d{9}$(不含国际区号时)。
  • 注册信息传递推荐用ViewModel替代Intent,避免数据丢失或过大。

内容的提问来源于stack exchange,提问作者Muralidharan

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.17 07:45:20