Firebase signInWithPopup未返回auth/account-exists-with-different-credential问题
问题描述
我开发了一个同时支持邮箱密码登录与Google登录的页面,遇到异常情况:当用户先通过邮箱+密码完成注册,再使用Google登录时,signInWithPopup会直接移除该账号的邮箱密码登录方式,且全程未触发Firebase官方文档中提及的任何错误。我预期会收到auth/account-exists-with-different-credential错误,但实际并未返回。
单点登录按钮实现代码
import { btnVariants } from "./CustomButton"; import { app } from "../firebase/client"; import { getAuth, signInWithPopup, GoogleAuthProvider, TwitterAuthProvider } from 'firebase/auth'; import { getFirestore, doc, getDoc } from "firebase/firestore"; import { setInitialUserProfile } from "@/utils/helpers/setInitialUserProfile"; export function SingleSignIn({ redirectPath, type }) { const auth = getAuth(app); const db = getFirestore(app); const methods = [ { label: "Google", icon: "/images/icon-google.png", id: "google.com" }, { label: "Twitter", icon: "/images/icon-twitter.png", id: "twitter.com" }, ]; const handleSignIn = async (providerId) => { let provider; // 获取对应登录提供商 if (providerId === 'google.com') { provider = new GoogleAuthProvider(); } else if (providerId === 'twitter.com') { provider = new TwitterAuthProvider(); } provider.addScope('email'); try { const userCredential = await signInWithPopup(auth, provider) .then(function (result) { var user = result.user; var credential = result.credential; console.log('✅success', user, credential) }, function (error) { console.log('❌ failed', error) var email = error.email; var credential = error.credential; console.log('❌ failed', email, credential) if (error.code === 'auth/account-exists-with-different-credential') { auth.fetchSignInMethodsForEmail(email).then(function (providers) { console.log('❌ failed, already exists', providers) }); } }) } catch (error) { console.error(`Error signing in with ${providerId}:`, error); } } return ( <div className="flex flex-col"> <div className={`flex flex-col gap-2 w-full`}> {methods.map((method, n) => ( <div key={n} className="flex w-full flex-col gap-1 items-center"> <button onClick={() => handleSignIn(method.id)} className={`${btnVariants({ variant: "outline" })} w-full items-center justify-center`} type="button" disabled={method.disabled} > <div className="gap-2 flex items-center justify-center"> <img src={method.icon} alt="" className="w-4 h-4 mr-2" /> <span className="font-sans capitalize font-medium"> Continue with {method.label} </span> </div> </button> </div> ))} </div> <div className="relative my-6 md:my-8 xl:my-10"> <div className="absolute inset-0 flex items-center"> <span className="w-full border-t border-neutral-200 dark:border-neutral-700"></span> </div> <div className="relative flex justify-center text-xs"> <span className="bg-background px-2 lg:px-4 font-sans font-medium text-foreground">Or</span> </div> </div> </div> ) } // const idToken = await userCredential.user.getIdToken() // // 检查用户Profile是否已创建 // const userProfileRef = doc( // db, // "userProfiles", // userCredential.user.uid, // ); // const userProfile = await getDoc(userProfileRef); // // 如果不存在则创建初始Profile // if (!userProfile.data()) { // await setInitialUserProfile(userCredential); // } // // 登录到后端 // try { // const response = await fetch("/api/auth/signin", { // method: "GET", // headers: { // Authorization: `Bearer ${idToken}`, // }, // }); // const res = await response.json() // console.log('response', res) // console.log('redirectPath', redirectPath) // // 页面跳转 // if (redirectPath) window.location.assign(redirectPath); // window.location.assign("/dashboard"); // } catch (error) { // console.log("❌ 登录错误"); // }
问题分析与解决方案
核心问题
代码中同时混用了async/await和.then()/.catch()的错误处理方式,导致auth/account-exists-with-different-credential错误被.then()的回调吃掉,外层try/catch无法捕获,最终错误处理逻辑完全失效。Firebase在这种情况下会默认执行账号合并,直接移除原有的密码登录方式。
修复后的代码实现
统一使用async/await+try/catch处理流程,确保所有错误都能被正确捕获和处理:
import { btnVariants } from "./CustomButton"; import { app } from "../firebase/client"; import { getAuth, signInWithPopup, GoogleAuthProvider, TwitterAuthProvider, fetchSignInMethodsForEmail } from 'firebase/auth'; import { getFirestore, doc, getDoc } from "firebase/firestore"; import { setInitialUserProfile } from "@/utils/helpers/setInitialUserProfile"; export function SingleSignIn({ redirectPath, type }) { const auth = getAuth(app); const db = getFirestore(app); const methods = [ { label: "Google", icon: "/images/icon-google.png", id: "google.com" }, { label: "Twitter", icon: "/images/icon-twitter.png", id: "twitter.com" }, ]; const handleSignIn = async (providerId) => { let provider; // 获取对应登录提供商 if (providerId === 'google.com') { provider = new GoogleAuthProvider(); } else if (providerId === 'twitter.com') { provider = new TwitterAuthProvider(); } provider.addScope('email'); try { const result = await signInWithPopup(auth, provider); const user = result.user; const credential = result.credential; console.log('✅登录成功', user, credential); // 获取用户ID Token const idToken = await user.getIdToken(); // 检查用户Profile是否已创建 const userProfileRef = doc(db, "userProfiles", user.uid); const userProfile = await getDoc(userProfileRef); // 如果不存在则创建初始Profile if (!userProfile.data()) { await setInitialUserProfile(result); } // 登录到后端 const response = await fetch("/api/auth/signin", { method: "GET", headers: { Authorization: `Bearer ${idToken}`, }, }); const res = await response.json(); console.log('后端响应', res); // 页面跳转 if (redirectPath) { window.location.assign(redirectPath); } else { window.location.assign("/dashboard"); } } catch (error) { console.error(`使用${providerId}登录失败:`, error); // 处理账号已存在的错误 if (error.code === 'auth/account-exists-with-different-credential') { const email = error.email; const pendingCredential = error.credential; // 获取该邮箱已关联的登录方式 const providers = await fetchSignInMethodsForEmail(auth, email); console.log('❌ 账号已存在,关联的登录方式:', providers); // 示例逻辑:如果关联了密码登录,提示用户输入密码后合并账号 if (providers.includes('password')) { // 这里可以替换为自定义的密码输入弹窗 // const password = prompt('请输入该邮箱的登录密码,完成账号关联'); // if (password) { // const passwordCred = await signInWithEmailAndPassword(auth, email, password); // await linkWithCredential(passwordCred.user, pendingCredential); // console.log('✅ 账号关联成功'); // } } } } } return ( <div className="flex flex-col"> <div className={`flex flex-col gap-2 w-full`}> {methods.map((method, n) => ( <div key={n} className="flex w-full flex-col gap-1 items-center"> <button onClick={() => handleSignIn(method.id)} className={`${btnVariants({ variant: "outline" })} w-full items-center justify-center`} type="button" disabled={method.disabled} > <div className="gap-2 flex items-center justify-center"> <img src={method.icon} alt="" className="w-4 h-4 mr-2" /> <span className="font-sans capitalize font-medium"> 继续使用 {method.label} </span> </div> </button> </div> ))} </div> <div className="relative my-6 md:my-8 xl:my-10"> <div className="absolute inset-0 flex items-center"> <span className="w-full border-t border-neutral-200 dark:border-neutral-700"></span> </div> <div className="relative flex justify-center text-xs"> <span className="bg-background px-2 lg:px-4 font-sans font-medium text-foreground">或</span> </div> </div> </div> ) }
关键修改说明
- 统一错误处理:移除
.then()/.catch()回调,全部使用async/await+try/catch,确保所有Firebase错误都能被捕获 - 恢复完整流程:将原本注释掉的用户Profile创建、后端登录逻辑移到
try块内,保证登录流程完整 - 正确处理账号冲突:在
catch块中针对auth/account-exists-with-different-credential错误添加了明确处理逻辑,可根据业务需求实现账号关联或提示用户切换登录方式
额外注意事项
- 确保Firebase控制台的Authentication模块中,邮箱/密码和Google登录方式均已启用
- 检查是否开启了"自动合并账号"功能(默认关闭,若开启可能导致错误不触发)
- 测试时需保证Google账号邮箱与之前注册的邮箱完全一致
内容的提问来源于stack exchange,提问作者Jay
相关产品推荐
相关产品推荐

