如何在Cloud Firestore安全规则中实施写入速率限制?

Fra*_*len 9 google-cloud-firestore firebase-security-rules

我有一个使用Firebase SDK直接从应用程序内部与Cloud Firestore对话的应用程序。我的代码确保仅以合理的间隔写入数据。但是恶意用户可能会从我的应用程序中获取配置数据,并使用它将无尽的数据流写入我的数据库。

我如何确保用户每隔几秒钟只能写一次发言,而不必编写任何服务器端代码。

Fra*_*len 17

对数据库的每次读或写操作都将根据您为项目配置的安全规则在Google的服务器上进行验证。这些规则只能由项目的协作者设置,但适用于访问项目中数据库的所有客户端代码。这意味着您可以在这些安全规则中强制执行此条件,即使恶意用户也无法绕过它们,因为它们无权访问您的项目。

假设我们有一个users集合,并且那里的每个文档都有一个带有用户UID的ID。这些安全规则确保用户只能写自己的文档,并且每5秒只能写一次:

match /users/{document=**} {
  allow create: if isMine() && hasTimestamp();
  allow update: if isMine() && hasTimestamp() && isCalm();
  function isMine() {
    return request.resource.id == request.auth.uid;
  }
  function hasTimestamp() {
    return request.resource.data.timestamp == request.time;
  }
  function isCalm() {
    return request.time > resource.data.timestamp + duration.value(5, 's');
  }
}
Run Code Online (Sandbox Code Playgroud)

演练可能会有所帮助:

  1. 第一行确定其中规则的范围,因此这些规则适用于/users集合中的所有文档。

  2. 用户可以创建自己的文档(isMine()),如果带有时间戳(hasTimestamp()),则可以创建该文档。

  3. 用户可以更新文档(如果有的话),带有时间戳记并且不经常写(isCalm())。

    让我们依次看一下这三个功能...

  4. isMine()功能检查文档ID是否与执行写操作的用户相同。由于auth.uidFirebase是根据登录的用户自动填充的,因此恶意用户无法欺骗该值。

  5. hasTimestamp()函数检查正在写入的文档(request.resource)是否具有时间戳字段,如果有,则该时间戳是否与当前服务器端时间相同。这意味着在代码中,需要指定FieldValue.serverTimestamp()才能使写入可接受。因此,您只能编写当前的服务器端时间戳,而恶意用户则无法传递其他时间戳。

  6. isCalm()功能确保用户不会经常写作。如果timestamp现有文档(resource.data.timestamp)和request.resource.data.timestamp当前正在写入的文档()中的值之间的差异至少为5秒,则允许写入。

Per Doug的评论:

重要的是要注意,以上实现的是每个文档的写限制,而不是每个帐户的写限制。用户仍然可以自由地以系统允许的速度编写其他文档。

如果要对他们写的所有文档有按用户的写速率限制,请继续阅读。


这是我测试这些规则的方式的jsbin:https ://jsbin.com/kejobej/2/edit?js,console 。使用此代码:

firebase.auth().signInAnonymously().then(function(auth) {
  var doc = collection.doc(auth.user.uid);
  doc.set({
    timestamp: firebase.firestore.FieldValue.serverTimestamp()
  }).then(function() {
    console.log("Written at "+new Date());
  }).catch(function(error) {
    console.error(error.code);
  })
})
Run Code Online (Sandbox Code Playgroud)

如果您反复单击该Run按钮,则仅在自上一次写入后至少经过5秒后才允许下一次写入。

当我大约每秒单击一次“运行”按钮时,我得到:

“写在2019年6月6日星期四20:20:19 GMT-0700(太平洋夏令时间)”

“没有权限”

“没有权限”

“没有权限”

“没有权限”

“写在2019年6月6日星期四20:20:24 GMT-0700(太平洋夏令时间)”

“没有权限”

“没有权限”

“没有权限”

“没有权限”

“写于2019年6月6日星期四20:20:30 GMT-0700(太平洋夏令时间)”


最后一个示例是每个用户的写入速率限制。假设您有一个社交媒体应用程序,用户在其中创建帖子,每个用户都有一个个人资料。因此,我们有两个集合:postsusers。并且我们希望确保用户最多每5秒创建一次新帖子。

规则与以前几乎相同,如下所示:用户可以更新自己的个人资料,如果过去5秒钟内没有写任何帖子,则可以创建一个帖子。

最大的不同是/users/$uid,即使他们正在创建新的帖子文档(/posts/$newid),我们也将时间戳存储在他们的用户个人资料()中。由于这两次写操作必须合而为一,因此我们将使用BatchedWrite一次遍历:

var root = firebase.firestore();
var users = root.collection("users");
var posts = root.collection("posts");

firebase.auth().signInAnonymously().then(function(auth) {
  var batch = db.batch();
  var userDoc = users.doc(auth.user.uid);
  batch.set(userDoc, {
    timestamp: firebase.firestore.FieldValue.serverTimestamp()
  })
  batch.set(posts.doc(), { 
    title: "Hello world"
  });
  batch.commit().then(function() {
    console.log("Written at "+new Date());
  }).catch(function(error) {
    console.error(error.code);
  })
})
Run Code Online (Sandbox Code Playgroud)

因此,该批处理写了两件事:

  • 它将当前服务器端时间写入用户的配置文件。
  • 它创建一个带有标题字段的新帖子。

如前所述,用于此目的的顶级安全规则与以前几乎相同:

match /users/{user} {
  allow write: if isMine() && hasTimestamp();
}
match /posts/{post} {
    allow write: if isCalm();
}
Run Code Online (Sandbox Code Playgroud)

因此,用户可以写入个人资料文档(如果该文档是自己的),并且该文档包含等于当前服务器端/请求时间的时间戳。如果用户最近没有发布过帖子,则可以写一个帖子。

和的实现isMine()hasTimstamp()以前相同。但是isCalm()now 的实现现在在写操作之前和之后查找用户概要文件,以进行时间戳检查:

function isCalm() {
    return getAfter(/databases/$(database)/documents/users/$(request.auth.uid)).data.timestamp
              > get(/databases/$(database)/documents/users/$(request.auth.uid)).data.timestamp + duration.value(5, 's');
}
Run Code Online (Sandbox Code Playgroud)

到PATH get()getAfter()遗憾的是必须是绝对的,完全合格的,但它归结为:

// These won't work, but are easier to read.    
function isCalm() {
  return getAfter(/users/$(request.auth.uid)).data.timestamp
            > get(/users/$(request.auth.uid)).data.timestamp + duration.value(5, 's');
}
Run Code Online (Sandbox Code Playgroud)

注意事项:

  • 就像在我们比较两个时间戳之前一样。但是这里我们从不同的文档中读取时间戳。
  • 这需要阅读两个额外的文档,这意味着您将需要为两个额外的阅读操作付费。如果不为恶意用户的写操作收取费用限制的目的,那么这可能不是您想要的解决方案。

  • “如果速率限制的目的是不为恶意用户的写入操作付费,那么这可能不是您正在寻找的解决方案。” 您将如何在 firestore 中做到这一点,即每个用户的速率限制而无需额外的读取或写入?对于这个问题我还没有找到满意的答案。 (15认同)
  • Nvm 我应该在问之前先看看。根据[文档](https://firebase.google.com/docs/firestore/manage-data/transactions),对于批次/交易来说,它要么全有,要么全无,所以现在我看到你的“isCalm”方法将如何拒绝新的如果客户端尝试跳过对用户配置文件中时间戳的更新,则发布 (3认同)
  • 有没有办法限制读取速率?我认为仅防止写入是不够的。 (3认同)
  • 速率限制主要是为了避免滥用或对api用户限制配额,不能用于屏蔽DDoS或任何高流量的重大攻击。即使我没有找到正确的答案,因为添加 DDoS 层会增加另一个级别的延迟。 (2认同)