如何合并Firebase的邮箱与谷歌身份验证提供商
问题描述
我先通过邮箱和密码注册了新用户,登录页面同时支持邮箱密码登录和谷歌登录功能。邮箱密码登录可正常运行,但使用同一邮箱进行谷歌登录后,账号的身份验证提供商被更新为谷歌,此后无法再通过邮箱密码登录,报错信息为User does not exist or user does not have a password。需要实现两种身份验证提供商的合并,让同一账号既可以用谷歌登录,也能继续用邮箱密码登录。
以下是原登录Activity代码:
open class LogInActivity : AppCompatActivity() { private lateinit var auth: FirebaseAuth private lateinit var googleSignInClient: GoogleSignInClient private lateinit var oneTapClient: SignInClient companion object { private const val RC_SIGN_IN = 123 private const val TAG = "LogInActivity" } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_log_in) auth = Firebase.auth oneTapClient = Identity.getSignInClient(this) btn_logIn.setOnClickListener { signInWithEmailPass() } val gso = GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN) .requestIdToken(getString(R.string.default_web_client_id)) .requestEmail() .build() googleSignInClient = GoogleSignIn.getClient(this, gso) tv_forgotPass.setOnClickListener { forgotPassword() } linear_layout.setOnClickListener { signInWithGoogle() } tv_register.setOnClickListener { val intent = Intent(this, SignUp::class.java) startActivity(intent) finish() } } private fun forgotPassword() { val intent = Intent(this, ResetPassword::class.java) startActivity(intent) } private fun signInWithGoogle() { val signInIntent = googleSignInClient.signInIntent startActivityForResult(signInIntent, RC_SIGN_IN) } override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { super.onActivityResult(requestCode, resultCode, data) if(requestCode == RC_SIGN_IN){ progressBar_LI.visibility = View.VISIBLE val task = GoogleSignIn.getSignedInAccountFromIntent(data) handleSignInResult(task) } } private fun handleSignInResult(task: Task<GoogleSignInAccount>) { try{ if(task.isSuccessful) { val account = task.getResult(ApiException::class.java) Log.d(TAG, "FirebaseAuthWithGoogle " + account.id) firebaseAuthWithGoogle(account.idToken) } else { progressBar_LI.visibility = View.GONE } } catch (e: ApiException){ Log.w(TAG, "SignInResult: Failed Code = " + e.statusCode ) } } private fun firebaseAuthWithGoogle(idToken: String?) { val credential = GoogleAuthProvider.getCredential(idToken, null) GlobalScope.launch(Dispatchers.IO){ val auth = auth.signInWithCredential(credential).await() //update UI in main thread because we can not update it in io thread withContext(Dispatchers.Main){ updateUI() } } } private fun signInWithEmailPass() { progressBar_LI.visibility = View.VISIBLE val email = et_email_LA.editText?.text.toString() val pass = et_password_LA.editText?.text.toString() if(email.isNotEmpty() && pass.isNotEmpty()) { if(pass.length < 8){ et_password_LA.error = "Password must contains at least 8 characters!" et_password_LA.requestFocus() progressBar_LI.visibility = View.GONE } else { auth.signInWithEmailAndPassword(email, pass).addOnCompleteListener { if (it.isSuccessful) { updateUI() } else { Toast.makeText(this, it.exception?.message.toString(), Toast.LENGTH_LONG) .show() progressBar_LI.visibility = View.GONE } } } } else { Toast.makeText(this, "Empty fields!!", Toast.LENGTH_SHORT).show() progressBar_LI.visibility = View.GONE } } override fun onStart() { super.onStart() val currentUser = auth.currentUser if (currentUser != null) { updateUI() } } private fun updateUI() { val intent = Intent(this, HomeActivity::class.java) startActivity(intent) finish() } }
原因分析
Firebase Auth中,当使用谷歌登录时,如果目标邮箱已关联一个邮箱密码类型的账号,默认不会自动合并身份提供商,而是抛出FirebaseAuthUserCollisionException异常。若未处理该异常,系统会将账号的登录方式替换为谷歌,导致原邮箱密码凭证失效,后续用邮箱密码登录时就会出现目标报错。
解决方案
在谷歌登录的认证流程中捕获账号冲突异常,将谷歌身份凭证关联到已有的邮箱密码账号上,实现两种登录方式的合并。具体实现步骤:
- 在谷歌登录的
signInWithCredential调用中捕获FirebaseAuthUserCollisionException; - 提示用户输入已有账号的密码,验证身份;
- 验证通过后,使用
linkWithCredential方法将谷歌凭证绑定到已有账号; - 绑定成功后,即可通过两种方式登录同一账号。
修改后的核心代码
重点修改firebaseAuthWithGoogle函数,添加异常处理与账号合并逻辑:
private fun firebaseAuthWithGoogle(idToken: String?) { val credential = GoogleAuthProvider.getCredential(idToken, null) auth.signInWithCredential(credential) .addOnCompleteListener(this) { task -> if (task.isSuccessful) { updateUI() progressBar_LI.visibility = View.GONE } else { val exception = task.exception if (exception is FirebaseAuthUserCollisionException) { // 检测到账号冲突,提示用户验证已有账号密码 showPasswordVerificationDialog(credential) } else { Toast.makeText(this, exception?.message.toString(), Toast.LENGTH_LONG).show() progressBar_LI.visibility = View.GONE } } } } // 弹出密码验证对话框,验证后合并账号 private fun showPasswordVerificationDialog(googleCredential: AuthCredential) { val email = googleCredential.email ?: return val builder = AlertDialog.Builder(this) builder.setTitle("验证已有账号") builder.setMessage("请输入${email}对应的密码以合并账号") val passwordInput = EditText(this) passwordInput.inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_PASSWORD builder.setView(passwordInput) builder.setPositiveButton("确认") { _, _ -> val password = passwordInput.text.toString() // 先通过邮箱密码登录已有账号 auth.signInWithEmailAndPassword(email, password) .addOnCompleteListener { signInTask -> if (signInTask.isSuccessful) { // 登录成功后,关联谷歌凭证 auth.currentUser?.linkWithCredential(googleCredential) ?.addOnCompleteListener { linkTask -> if (linkTask.isSuccessful) { Toast.makeText(this, "账号合并成功", Toast.LENGTH_SHORT).show() updateUI() } else { Toast.makeText(this, "关联失败:${linkTask.exception?.message}", Toast.LENGTH_LONG).show() } progressBar_LI.visibility = View.GONE } } else { Toast.makeText(this, "密码错误:${signInTask.exception?.message}", Toast.LENGTH_LONG).show() progressBar_LI.visibility = View.GONE } } } builder.setNegativeButton("取消") { dialog, _ -> dialog.dismiss() progressBar_LI.visibility = View.GONE } builder.show() }
额外说明
- 合并账号后,该邮箱对应的账号会同时拥有邮箱密码和谷歌两种登录凭证;
- 若用户先通过谷歌登录创建账号,后续想添加邮箱密码登录,可调用
currentUser?.updatePassword()方法为账号设置密码; - 若使用协程处理异步操作,需在
try-catch块中捕获FirebaseAuthUserCollisionException异常,逻辑与上述回调方式一致。
内容的提问来源于stack exchange,提问作者Divyansh Singh
相关产品推荐
相关产品推荐

