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

React项目中Firestore实时监听器与安全规则冲突问题

Firestore实时监听器与安全规则竞态条件:新文档创建前触发权限校验导致拒绝

问题背景

在React项目中使用Firestore时遇到竞态条件问题:创建adventures集合的新文档后,实时监听器会立即感知到本地缓存中的文档,并为该文档的子集合layers创建监听器,但此时后端的父文档尚未完成写入,导致安全规则校验失败,返回权限被拒。

触发流程

  • Firebase SDK发起新文档创建请求
  • 应用通过实时监听器立即获取到本地缓存的新文档
  • 应用自动为该文档的layers子集合创建监听器
  • Firestore规则引擎尝试读取父文档的字段进行权限校验
  • 由于父文档尚未在后端落地,get()操作失败,权限校验被拒
  • 父文档在后端创建完成,但此时权限校验已失败

项目代码与规则详情

创建Adventure的代码

import {addDoc, collection} from "firebase/firestore";
[...]
const adventuresRef = collection(firestore,"adventures")
const adventureId = (await addDoc(adventuresRef, newAdventure)).id;  

监听用户Adventures的代码

import { useFirestoreCollectionData } from "reactfire";  
[...]
const firestoreQuery = query(adventuresRef, where("userId", "==", userId), orderBy("createdAt"));  
const { data: userAdventures } = useFirestoreCollectionData(firestoreQuery, {  
  idField: "id",  
});

安全规则核心逻辑

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    function getAdventure(adventureId) {
      return get(/databases/$(database)/documents/adventures/$(adventureId)).data
    }
    function getIsOwnerOrIsPublic(adventureId) {
      let adventure = getAdventure(adventureId);
      let isOwner = adventure.userId == request.auth.uid;
      let isPublic = adventure.isPublic == true;
      return isOwner || isPublic
    }
    function getCanRead(adventureId) {
      return getIsOwnerOrIsPublic(adventureId) || [...]
    }
    match /adventures/{adventureId} {
      match /layers/{layerId} {
        allow read: if getCanRead(adventureId);
      }
    }
  }
}

当前临时方案是通过Node.js后端创建adventure,绕过实时更新,现寻求更优的前端侧或规则侧解决方案。


解决方案

方案1:过滤本地未提交文档,延迟创建子集合监听器

Firestore文档快照包含metadata.hasPendingWrites属性,用于标识文档是否仅存在于本地缓存、尚未提交到后端。我们可以利用这个属性过滤未确认的文档,仅为已落地的文档创建子集合监听器:

import { useFirestoreCollectionData } from "reactfire";
import { query, where, orderBy } from "firebase/firestore";
[...]
const firestoreQuery = query(adventuresRef, where("userId", "==", userId), orderBy("createdAt"));
// 开启元数据变化监听,获取文档提交状态
const { data: adventureSnapshots } = useFirestoreCollectionData(firestoreQuery, {
  idField: "id",
  snapshotListenOptions: { includeMetadataChanges: true }
});

// 只保留已提交到后端的文档
const userAdventures = adventureSnapshots?.filter(snap => !snap.metadata.hasPendingWrites);

之后仅基于userAdventures中的文档创建layers子集合的监听器,即可避免父文档未落地时的权限校验失败。

方案2:修改安全规则,处理父文档不存在的场景

在安全规则中先判断父文档是否存在,若不存在则针对当前认证用户开放临时权限(仅适用于创建者触发的场景):

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    function getAdventure(adventureId) {
      return get(/databases/$(database)/documents/adventures/$(adventureId)).data
    }
    function getIsOwnerOrIsPublic(adventureId) {
      let adventureRef = /databases/$(database)/documents/adventures/$(adventureId);
      // 先检查父文档是否存在
      if (exists(adventureRef)) {
        let adventure = get(adventureRef).data;
        let isOwner = adventure.userId == request.auth.uid;
        let isPublic = adventure.isPublic == true;
        return isOwner || isPublic;
      } else {
        // 父文档不存在时,仅允许当前认证用户访问(仅创建者会触发此场景)
        return request.auth != null;
      }
    }
    function getCanRead(adventureId) {
      return getIsOwnerOrIsPublic(adventureId) || [...]
    }
    match /adventures/{adventureId} {
      match /layers/{layerId} {
        allow read: if getCanRead(adventureId);
      }
    }
  }
}

方案3:将父文档权限信息嵌入子集合文档

在创建layers子文档时,将父文档的userId和isPublic字段复制到子文档中,让安全规则无需依赖父文档即可完成校验:

创建Layer的代码示例

import { addDoc, collection } from "firebase/firestore";
[...]
const createLayer = async (adventureId, newLayer) => {
  // 复制父文档的权限字段到子文档
  const layerWithPermissions = {
    ...newLayer,
    userId: currentUser.uid,
    isPublic: false // 可从父文档获取实际值
  };
  const layersRef = collection(firestore, `adventures/${adventureId}/layers`);
  await addDoc(layersRef, layerWithPermissions);
};

修改后的安全规则

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /adventures/{adventureId}/layers/{layerId} {
      allow read: if resource.data.userId == request.auth.uid || resource.data.isPublic == true;
    }
  }
}

内容的提问来源于stack exchange,提问作者Louis Eugene

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.27 06:23:13