Firebase v9中如何安全创建与auth.user数据库同步的onCreate用户文档及解决同步异常问题
解决Firebase Auth与Firestore用户文档同步及更新错误问题
你的问题核心在于Auth用户创建事件的云函数与前端操作存在异步延迟:createUserWithEmailAndPassword完成后立即触发onCreate云函数,但云函数创建Firestore文档需要时间,而前端紧接着就尝试更新该文档,自然会出现No document to update的错误。同时,onCreate触发时,你刚通过updateProfile设置的displayName还没同步到Auth用户对象中,导致云函数初始化的文档缺少这些字段。
下面提供几种针对性的解决方案,按推荐优先级排序:
方案1:通过Auth onUpdate事件自动同步用户资料(最推荐)
完全交给云函数处理数据同步,避免前端与云函数的竞争,同时符合你“不让用户自行创建文档”的安全需求:
- 保留现有
onCreate云函数,负责初始化用户基础信息:
export const authUserWriteListener = functions.auth .user() .onCreate(async (user, context) => { const userRef = db.doc(`users/${user.uid}`); await userRef.set({ email: user.email, createdAt: context.timestamp, firstTimeLogin: true, }); return db.doc("stats/users").update({ totalDocsCount: FieldValue.increment(1), }); });
- 新增
onUpdate云函数,监听Auth用户资料变化,自动同步到Firestore:
export const authUserUpdateListener = functions.auth .user() .onUpdate(async (change, context) => { const beforeUser = change.before; const afterUser = change.after; // 仅当displayName发生变化时同步 if (beforeUser.displayName !== afterUser.displayName) { const userRef = db.doc(`users/${afterUser.uid}`); // 拆分displayName为firstName和lastName(根据你的需求调整) const [firstName, lastName] = afterUser.displayName?.split(' ') || ['', '']; await userRef.update({ displayName: afterUser.displayName, firstName, lastName }); } // 可扩展:添加photoURL等其他字段的同步逻辑 return null; });
- 前端代码简化,无需处理Firestore文档更新:
const handleSignUp = async (values: SignUpFormValues) => { const { firstName, lastName, email, password } = values; const displayName = `${firstName} ${lastName}`; try { setError(""); setLoading(true); // 创建Auth用户 const userCredential = await createUserWithEmailAndPassword(auth, email, password); const user = userCredential.user; // 更新Auth用户资料 await updateProfile(user, { displayName }); // 强制刷新令牌 await auth.currentUser.getIdToken(true); // 发送邮箱验证邮件 await authSendEmailVerification(user); // 登出并跳转 await logout(); navigate("/sign-up/email-verification", { state: values }); } catch (error: any) { const errorCode = error.code; if (errorCode === "auth/email-already-in-use") { setError("Failed to create an account. E-Mail address is already registered."); } else { setError("Failed to create account."); } } setLoading(false); };
这个方案的优势是:后端完全掌控数据同步,避免前端操作的安全风险,同时彻底解决异步延迟问题。
方案2:前端使用事务确保文档存在后更新
如果希望前端直接处理文档更新,可以用Firestore事务处理——事务会自动检查文档状态,不存在则创建,存在则更新:
修改前端中更新文档的逻辑:
// 替换原有的getDoc和updateDoc代码 const newUserData = { displayName, firstName, lastName, }; await runTransaction(db, async (transaction) => { const docRef = doc(db, "users", user.uid); const docSnap = await transaction.get(docRef); if (docSnap.exists()) { transaction.update(docRef, newUserData); } else { // 若云函数还未创建文档,前端直接创建(需配合安全规则) transaction.set(docRef, { ...newUserData, email: user.email, createdAt: FieldValue.serverTimestamp(), firstTimeLogin: true }); } });
同时调整Firestore安全规则,允许用户操作自己的文档:
rules_version = '2'; service cloud.firestore { match /databases/{database}/documents { match /users/{userId} { allow read, write: if request.auth != null && request.auth.uid == userId; } } }
这个方案适合需要前端立即同步数据的场景,但需确保安全规则的正确性,防止用户篡改他人文档。
方案3:前端监听文档创建事件,等待云函数完成
如果不想修改安全规则,可以让前端监听用户文档的创建事件,等云函数完成初始化后再更新:
// 替换原有的getDoc和updateDoc代码 const docRef = doc(db, "users", user.uid); // 监听文档创建 const unsubscribe = onSnapshot(docRef, (docSnap) => { if (docSnap.exists()) { unsubscribe(); // 取消监听 // 更新文档 const newUserData = { displayName, firstName, lastName }; updateDoc(docRef, newUserData) .then(() => { // 继续后续流程 authSendEmailVerification(user) .then(() => logout()) .then(() => navigate("/sign-up/email-verification", { state: values })); }) .catch((err) => { console.error("更新用户文档失败:", err); setError("Failed to complete sign-up process."); setLoading(false); }); } }, (err) => { console.error("监听用户文档失败:", err); unsubscribe(); setError("Failed to complete sign-up process."); setLoading(false); });
注意:这种方式需要调整前端流程,将验证邮件、登出等操作放在监听回调中,避免过早执行。
内容的提问来源于stack exchange,提问作者BenjaminK
相关产品推荐
相关产品推荐

