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

Dou*_*son 2 firebase firebase-security google-cloud-firestore

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

可以通过后端强制客户端强制执行此操作,或使用 Cloud Functions 触发器自动维护计数(这将在一些延迟后生效)。但是,我不想引入后端,我宁愿使用安全规则。我怎样才能做到这一点?

Dou*_*son 6

想象一下,您有一个“消息”集合,其中包含客户端可以添加和删除的消息。还可以想象不同集合中的文档,其路径为“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)
    }
}
Run Code Online (Sandbox Code Playgroud)

或者像这样的一批:

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)
    }
}
Run Code Online (Sandbox Code Playgroud)

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

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)
    }
}
Run Code Online (Sandbox Code Playgroud)

或者像这样批量处理:

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)
    }
}
Run Code Online (Sandbox Code Playgroud)

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

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))
      );
    }

  }
}
Run Code Online (Sandbox Code Playgroud)

请注意,客户端必须:

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

需要注意的是existsAfter()检查指定文件的交易后的状态完成,而存在的()之前检查它。这两个函数之间的差异对于这些规则的工作方式很重要。

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

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

match /messages/{id} {
  allow create: if
    get(/databases/$(database)/documents/messages-stats/data).data.count < 5;
}
Run Code Online (Sandbox Code Playgroud)