代码之家  ›  专栏  ›  技术社区  ›  Doug Stevenson

如何强制Firestore客户端应用程序为集合维护正确的文档计数?

  •  0
  • Doug Stevenson  · 技术社区  · 5 年前

    Firestore无法查询集合的大小,因此如果客户端应用程序需要知道此值,则需要对另一个集合中的文档进行一些维护来保存此计数。但是,它要求客户正确执行交易,以便在添加和删除文档时保持此计数的最新状态。恶意或损坏的客户端可以独立修改集合或计数,并造成计数不准确的情况。

    可以通过后端强制客户端执行此操作,或者使用Cloud Functions触发器自动维护计数(这将在一段时间后生效)。但是,我不想引入后端,我宁愿使用安全规则。我该怎么做?

    1 回复  |  直到 5 年前
        1
  •  8
  •   Doug Stevenson    5 年前

    想象一下,你有一个“消息”集合,其中包含客户端可以添加和删除的消息。还要想象一个不同集合中的文档,路径为“messages stats/data”,字段为“count”,用于维护消息中文档的准确计数。如果客户端应用程序执行这样的事务以添加文档:

    async function addDocumentTransaction() {
        try {
            const ref = firestore.collection("messages").doc()
            const statsRef = firestore.collection("messages-stats").doc("data")
            await firestore.runTransaction(transaction => {
                transaction.set(ref, {
                    foo: "bar"
                })
                transaction.update(statsRef, {
                    count: firebase.firestore.FieldValue.increment(1),
                    messageId: ref.id
                })
                return Promise.resolve()
            })
            console.log(`Added message ${ref.id}`)
        }
        catch (error) {
            console.error(error)
        }
    }
    

    或者像这样的批次:

    async function addDocumentBatch() {
        try {
            const batch = firestore.batch()
            const ref = firestore.collection("messages").doc()
            const statsRef = firestore.collection("messages-stats").doc("data")
            batch.set(ref, {
                foo: "bar"
            })
            batch.update(statsRef, {
                count: firebase.firestore.FieldValue.increment(1),
                messageId: ref.id
            })
            await batch.commit()
            console.log(`Added message ${ref.id}`)
        }
        catch (error) {
            console.error(error)
        }
    }
    

    像这样使用事务删除文档:

    async function deleteDocumentTransaction(id) {
        try {
            const ref = firestore.collection("messages").doc(id)
            const statsRef = firestore.collection("messages-stats").doc("data")
            await firestore.runTransaction(transaction => {
                transaction.delete(ref)
                transaction.update(statsRef, {
                    count: firebase.firestore.FieldValue.increment(-1),
                    messageId: ref.id
                })
                return Promise.resolve()
            })
            console.log(`Deleted message ${ref.id}`)
        }
        catch (error) {
            console.error(error)
        }
    }
    

    或者像这样处理一批:

    async function deleteDocumentBatch(id) {
        try {
            const batch = firestore.batch()
            const ref = firestore.collection("messages").doc(id)
            const statsRef = firestore.collection("messages-stats").doc("data")
            batch.delete(ref)
            batch.update(statsRef, {
                count: firebase.firestore.FieldValue.increment(-1),
                messageId: ref.id
            })
            await batch.commit()
            console.log(`Deleted message ${ref.id}`)
        }
        catch (error) {
            console.error(error)
        }
    }
    

    然后,您可以使用安全规则要求添加或删除的文档只能与具有计数字段的文档同时更改。最低限度:

    rules_version = '2';
    service cloud.firestore {
      match /databases/{database}/documents {
    
        match /messages/{id} {
          allow read;
          allow create: if
            getAfter(/databases/$(database)/documents/messages-stats/data).data.count ==
                 get(/databases/$(database)/documents/messages-stats/data).data.count + 1;
          allow delete: if
            getAfter(/databases/$(database)/documents/messages-stats/data).data.count ==
                 get(/databases/$(database)/documents/messages-stats/data).data.count - 1;
        }
    
        match /messages-stats/data {
          allow read;
          allow update: if (
            request.resource.data.count == resource.data.count + 1 &&
            existsAfter(/databases/$(database)/documents/messages/$(request.resource.data.messageId)) &&
               ! exists(/databases/$(database)/documents/messages/$(request.resource.data.messageId))
          ) || (
            request.resource.data.count == resource.data.count - 1 &&
            ! existsAfter(/databases/$(database)/documents/messages/$(request.resource.data.messageId)) &&
                   exists(/databases/$(database)/documents/messages/$(request.resource.data.messageId))
          );
        }
    
      }
    }
    

    请注意,客户必须:

    • 递增或递减计数 /messages-stats/data 在添加或删除文档时。
    • 必须在名为的字段中提供在“数据”文档中添加或删除的文档的id messageId .
    • 递增计数要求中标识的新文档 messageId 在批处理/事务提交之前不得存在,在事务提交之后必须存在。
    • 减少计数要求中标识的旧文档 messageId 必须在批处理/事务提交之前存在,在事务提交之后不存在。

    请注意 existsAfter() 在事务处理后检查指定文档的状态 完成,同时 exists() 之前检查过。这两个函数之间的区别对这些规则的工作方式很重要。

    还要注意,在重载下,这将无法很好地扩展。如果文档的添加和删除速度超过每秒10个,则数据文档的每个文档写入速率将超过,事务将失败。

    一旦你有了这个,现在你实际上可以编写安全规则来限制集合的大小,如下所示:

    match /messages/{id} {
      allow create: if
        get(/databases/$(database)/documents/messages-stats/data).data.count < 5;
    }
    
        2
  •  2
  •   Marjan    5 年前

    建议的解决方案仍然会失败,因为您可以在批处理写入中添加其他文档。

    但您可以将以下内容添加到消息收集规则中:

    && get(/databases/$(database)/documents/messages-stats/data).data.messageId == request.resource.id;

    rules_version = '2';
    service cloud.firestore {
      match /databases/{database}/documents {
    
        match /messages/{id} {
          allow read;
          allow create: if
            getAfter(/databases/$(database)/documents/messages-stats/data).data.count ==
                 get(/databases/$(database)/documents/messages-stats/data).data.count + 1 
                 && get(/databases/$(database)/documents/messages-stats/data).data.messageId == request.resource.id;
          allow delete: if
            getAfter(/databases/$(database)/documents/messages-stats/data).data.count ==
                 get(/databases/$(database)/documents/messages-stats/data).data.count - 1
                 && get(/databases/$(database)/documents/messages-stats/data).data.messageId == request.resource.id;
        }
    
        match /messages-stats/data {
          allow read;
          allow update: if (
            request.resource.data.count == resource.data.count + 1 &&
            existsAfter(/databases/$(database)/documents/messages/$(request.resource.data.messageId)) &&
               ! exists(/databases/$(database)/documents/messages/$(request.resource.data.messageId))
          ) || (
            request.resource.data.count == resource.data.count - 1 &&
            ! existsAfter(/databases/$(database)/documents/messages/$(request.resource.data.messageId)) &&
                   exists(/databases/$(database)/documents/messages/$(request.resource.data.messageId))
          );
        }
    
      }
    }