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
相关产品推荐
相关产品推荐

