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

Cloud Firestore权限异常:一对一聊天读写权限拒绝问题排查

问题描述

我正在实现支持聊天列表拉取与实时一对一聊天的功能,为限制数据访问,配置了Cloud Firestore规则,要求仅登录用户可读写与自身uid相关的数据。但拉取chatlist数据及发送消息时,均触发错误:

Uncaught Error in snapshot listener: FirebaseError: [code=permission-denied]: Missing or insufficient permissions

原规则代码

rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /messages/{document=**} {
      allow read: if request.auth != null && (request.auth.uid == resource.data.uid || request.auth.uid == resource.data.recipientId);
    }
     match /messages/{document} {
    allow write: if request.auth != null && (request.auth.uid == resource.data.uid || request.auth.uid == resource.data.recipientId);
    }

    // Chatlist Collection
    match /chatlist/{document=**} {
      allow read: if request.auth != null && (request.auth.uid == resource.data.uid || request.auth.uid == resource.data.recipientId);
      allow write: if request.auth != null && (request.auth.uid == resource.data.uid || request.auth.uid == resource.data.recipientId);
    }
    match /chatlist/{document} {
     allow write: if request.auth != null && (request.auth.uid == resource.data.uid || request.auth.uid == resource.data.recipientId);
    }
    }
  }

原拉取聊天列表代码

const fetchData = async () => {
  try {
    const messagesQuery = query(
      collection(db, "messages"),
      orderBy("createdAt", "desc")
    );
    const messagesSnapshot = await getDocs(messagesQuery);
    const messagesData = messagesSnapshot.docs.map((doc) => doc.data());
    console.log(messagesData,"messagesData");
    const chatlistQuery = query(collection(db, "chatlist"));
    const chatlistSnapshot = await getDocs(chatlistQuery);
    const chatlistData = chatlistSnapshot.docs.map((doc) => doc.data());
    console.log(chatlistData,"chatlistData");
    const filteredData = chatlistData.filter(
      (item) =>
        item.recipientId === currentUserUID || item.uid === currentUserUID
    );
    const uniqueUsers = new Map();

    filteredData.forEach((item) => {
      const existingItem = uniqueUsers.get(
        item.senderName || item.userName
      );
      if (!existingItem || item.createdAt > existingItem.createdAt) {
        if (item.recipientId === currentUserUID) {
          item.senderName = item.userName;
          item.recipientId = item.uid;
          item.email = item.currentUserEmail;
          uniqueUsers.set(item.userName, item);
        } else {
          uniqueUsers.set(item.senderName, item);
        }
      }
    });

    const updatedRecentChat = Array.from(uniqueUsers.values()).map(
      (item) => {
        const recentMessages = messagesData.filter(
          (message) =>
            (message.uid === currentUserUID &&
              message.recipientId === item.recipientId) ||
            (message.uid === item.recipientId &&
              message.recipientId === currentUserUID)
        );

        const sortedMessages = recentMessages.sort(
          (a, b) => b.createdAt - a.createdAt
        );

        if (sortedMessages.length > 0) {
          const recentMessage = sortedMessages[0];
          return {
            ...item,
            recentMessage: recentMessage.text,
            recentMessageCreatedAt: recentMessage.createdAt,
          };
        } else {
          // If there are no recent messages, set an empty string for the recentMessage field
          return {
            ...item,
            recentMessage: "",
            recentMessageCreatedAt: null,
          };
        }
      }
    );

    const sortedRecentChat = updatedRecentChat.sort((a, b) => {
      if (a.recentMessageCreatedAt && b.recentMessageCreatedAt) {
        return b.recentMessageCreatedAt - a.recentMessageCreatedAt;
      } else if (a.recentMessageCreatedAt) {
        return -1;
      } else if (b.recentMessageCreatedAt) {
        return 1;
      } else {
        return 0;
      }
    });
    setRecentchat(sortedRecentChat);
  } catch (error) {
    return error;
  }
};

原发送消息代码

const sendMessage = async () => {
    const { uid, displayName, photoURL } = auth.currentUser;
    const newMessage = {
      text: input,
      name: displayName,
      avatar: photoURL,
      createdAt: serverTimestamp(),
      uid: uid,
      userName: userName,
      senderName: senderName,
      recipientId: recipientId,
    };
  
    try {
      await addDoc(collection(db, "messages"), newMessage);
      setInput("");
  
      const newChatListEntry = {
        uid: uid,
        recipientId: recipientId,
        userName: userName,
        senderName: senderName,
        email: email,
        currentUserEmail: currentUserEmail,
      };
  
      await addDoc(collection(db, "chatlist"), newChatListEntry);
      // Clear the message input field
  
    } catch (error) {
      console.error("Error adding document: ", error);
      // Handle the error as needed
    }
  };

问题排查与解决方案

1. Firestore规则核心问题

(1)写入规则依赖不存在的resource.data

使用addDoc创建新文档时,resource.data是未定义的(文档还未创建),原规则中request.auth.uid == resource.data.uid的条件永远不成立,直接导致写入权限被拒绝。

(2)规则重复定义导致冲突

原规则中对/messages/{document=**}和/messages/{document}重复定义,{document=**}是递归匹配所有子集合,普通{document}仅匹配集合下的文档,重复定义会引发规则逻辑冲突。

(3)全量读取未通过规则校验

拉取聊天列表时,代码先全量读取chatlist集合再前端过滤,但Firestore规则会先校验整个查询的合法性,全量读取请求无法证明所有文档都满足用户权限条件,直接被拦截。

2. 修正后的Firestore规则

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    // Messages集合规则
    match /messages/{messageId} {
      // 读取:登录用户且是消息发送方或接收方
      allow read: if request.auth != null && 
        (request.auth.uid == resource.data.uid || request.auth.uid == resource.data.recipientId);
      // 写入:登录用户且是消息发送方(创建时用request.resource.data获取待写入数据)
      allow write: if request.auth != null && 
        request.auth.uid == request.resource.data.uid;
    }

    // Chatlist集合规则
    match /chatlist/{chatId} {
      // 读取:登录用户且是聊天参与方
      allow read: if request.auth != null && 
        (request.auth.uid == resource.data.uid || request.auth.uid == resource.data.recipientId);
      // 写入:登录用户且是聊天参与方(创建时验证当前用户是uid或recipientId)
      allow write: if request.auth != null && 
        (request.auth.uid == request.resource.data.uid || request.auth.uid == request.resource.data.recipientId);
    }
  }
}

3. 修正后的拉取聊天列表代码

const fetchData = async () => {
  try {
    const currentUserUID = auth.currentUser?.uid;
    if (!currentUserUID) return;

    // 直接查询当前用户参与的chatlist文档,避免全量读取
    const chatlistQuery = query(
      collection(db, "chatlist"),
      or(
        where("uid", "==", currentUserUID),
        where("recipientId", "==", currentUserUID)
      )
    );
    const chatlistSnapshot = await getDocs(chatlistQuery);
    const chatlistData = chatlistSnapshot.docs.map((doc) => doc.data());
    console.log(chatlistData,"chatlistData");

    // 查询当前用户参与的消息,提前过滤权限范围内的数据
    const messagesQuery = query(
      collection(db, "messages"),
      or(
        where("uid", "==", currentUserUID),
        where("recipientId", "==", currentUserUID)
      ),
      orderBy("createdAt", "desc")
    );
    const messagesSnapshot = await getDocs(messagesQuery);
    const messagesData = messagesSnapshot.docs.map((doc) => doc.data());
    console.log(messagesData,"messagesData");

    const uniqueUsers = new Map();
    chatlistData.forEach((item) => {
      const targetName = item.recipientId === currentUserUID ? item.userName : item.senderName;
      const existingItem = uniqueUsers.get(targetName);
      if (!existingItem || (item.createdAt && existingItem.createdAt && item.createdAt > existingItem.createdAt)) {
        const processedItem = { ...item };
        if (item.recipientId === currentUserUID) {
          processedItem.senderName = item.userName;
          processedItem.recipientId = item.uid;
          processedItem.email = item.currentUserEmail;
        }
        uniqueUsers.set(targetName, processedItem);
      }
    });

    const updatedRecentChat = Array.from(uniqueUsers.values()).map((item) => {
      const recentMessages = messagesData.filter(
        (message) =>
          (message.uid === currentUserUID && message.recipientId === item.recipientId) ||
          (message.uid === item.recipientId && message.recipientId === currentUserUID)
      );
      const sortedMessages = recentMessages.sort((a, b) => b.createdAt - a.createdAt);
      if (sortedMessages.length > 0) {
        const recentMessage = sortedMessages[0];
        return {
          ...item,
          recentMessage: recentMessage.text,
          recentMessageCreatedAt: recentMessage.createdAt,
        };
      } else {
        return {
          ...item,
          recentMessage: "",
          recentMessageCreatedAt: null,
        };
      }
    });

    const sortedRecentChat = updatedRecentChat.sort((a, b) => {
      if (a.recentMessageCreatedAt && b.recentMessageCreatedAt) {
        return b.recentMessageCreatedAt - a.recentMessageCreatedAt;
      } else if (a.recentMessageCreatedAt) {
        return -1;
      } else if (b.recentMessageCreatedAt) {
        return 1;
      } else {
        return 0;
      }
    });
    setRecentchat(sortedRecentChat);
  } catch (error) {
    console.error("拉取数据失败:", error);
    return error;
  }
};

关键注意事项

  • 复合索引创建:使用or查询或where+orderBy组合时,Firestore会提示创建对应复合索引,直接点击控制台给出的链接创建即可。
  • request.resource与resource区别:创建新文档用request.resource.data获取待写入数据,更新/删除时用resource.data获取已存在文档的数据。
  • 禁止全量读取:所有读取请求必须在查询阶段过滤出当前用户有权访问的数据,否则规则会直接拦截请求。

内容的提问来源于stack exchange,提问作者am's

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.19 22:47:02