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

Firebase v9中如何安全创建与auth.user数据库同步的onCreate用户文档及解决同步异常问题

解决Firebase Auth与Firestore用户文档同步及更新错误问题

你的问题核心在于Auth用户创建事件的云函数与前端操作存在异步延迟:createUserWithEmailAndPassword完成后立即触发onCreate云函数,但云函数创建Firestore文档需要时间,而前端紧接着就尝试更新该文档,自然会出现No document to update的错误。同时,onCreate触发时,你刚通过updateProfile设置的displayName还没同步到Auth用户对象中,导致云函数初始化的文档缺少这些字段。

下面提供几种针对性的解决方案,按推荐优先级排序:

方案1:通过Auth onUpdate事件自动同步用户资料(最推荐)

完全交给云函数处理数据同步,避免前端与云函数的竞争,同时符合你“不让用户自行创建文档”的安全需求:

  1. 保留现有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),
    });
  });
  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;
  });
  1. 前端代码简化,无需处理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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.04.28 14:32:27