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
相关产品推荐
相关产品推荐

