安全地查询数据

本页面以设计安全规则结构编写安全规则的条件中的概念为基础,介绍 Firestore 安全规则与查询之间的相互作用。其中将更详细地介绍安全规则如何影响您可以编写的查询,以及如何确保您的查询与安全规则使用相同的限制条件。本页面还介绍了如何编写安全规则,以根据查询的属性(如 limitorderBy)允许或拒绝查询。

规则并非过滤条件

在编写查询以检索文档时,请务必留意,安全规则不是过滤器 - 查询结果要么包含全部,要么什么也没有。为了节省时间和资源,Firestore 会针对查询的可能结果集(而不是您所有文档的实际字段值)来对其进行评估。如果查询可能会返回客户端无权读取的文档,则整个请求将会失败。

查询和安全规则

如下例所示,您编写的查询必须符合您的安全规则的限制条件。

根据 auth.uid 保护和查询文档

以下示例演示了如何编写查询以检索受安全规则保护的文档。假设某数据库包含了一个 story 文档的集合:

/stories/{storyid}

{
  title: "A Great Story",
  content: "Once upon a time...",
  author: "some_auth_id",
  published: false
}

除了 titlecontent 字段之外,每个文档还存储了用于控制访问的 authorpublished 字段。这些示例假定应用使用 Firebase Authenticationauthor 字段设置为创建文档的用户的 UID。Firebase Authentication 还会填充安全规则中的 request.auth 变量。

以下安全规则使用 request.authresource.data 变量限定每个 story 的读写权限仅归其作者所有:

service cloud.firestore {
  match /databases/{database}/documents {
    match /stories/{storyid} {
      // Only the authenticated user who authored the document can read or write
      allow read, write: if request.auth != null && request.auth.uid == resource.data.author;
    }
  }
}

假设您的应用包含一个页面,其中向用户列出他们所创建的 story 文档。您也许会认为,可以使用以下查询来填充此页面。但是,由于没有包含与您的安全规则相同的限制条件,此查询将会失败:

无效:查询限制条件与安全规则限制条件不匹配

// This query will fail
db.collection("stories").get()

即使当前用户实际上是所有 story 文档的作者,查询也会失败。造成这种情况的原因是,当 Firestore 应用您的安全规则时,它会针对可能的结果集评估查询,而不是针对数据库中文档的实际属性进行评估。如果查询可能包括违反安全规则的文档,则查询将会失败。

相反,由于在 author 字段中包含了与安全规则相同的限制条件,因此以下查询将会成功:

有效:查询和安全规则的限制条件匹配

var user = firebase.auth().currentUser;

db.collection("stories").where("author", "==", user.uid).get()

根据字段保护和查询文档

为了进一步演示查询和规则之间的相互作用,下面的安全规则扩展了 stories 集合的读取权限,以允许任意用户读取 published 字段设置为 truestory 文档。

service cloud.firestore {
  match /databases/{database}/documents {
    match /stories/{storyid} {
      // Anyone can read a published story; only story authors can read unpublished stories
      allow read: if resource.data.published == true || (request.auth != null && request.auth.uid == resource.data.author);
      // Only story authors can write
      allow write: if request.auth != null && request.auth.uid == resource.data.author;
    }
  }
}

针对已发布页面的查询必须包含与安全规则相同的限制条件:

db.collection("stories").where("published", "==", true).get()

对于任何结果,查询限制条件 .where("published", "==", true) 都保证 resource.data.publishedtrue。因此,该查询满足安全规则,可用于读取数据。

OR 查询

根据规则集评估逻辑 OR 查询(orinarray-contains-any)时,Firestore 会分别评估每个比较值。每个比较值都必须满足安全规则限制条件。例如,对于以下规则:

match /mydocuments/{doc} {
  allow read: if resource.data.x > 5;
}

无效:查询并不保证所有可能的文档都满足 x > 5

// These queries will fail
query(db.collection("mydocuments"),
      or(where("x", "==", 1),
         where("x", "==", 6)
      )
    )

query(db.collection("mydocuments"),
      where("x", "in", [1, 3, 6, 42, 99])
    )

有效:对于所有可能的文档,查询都保证 x > 5

query(db.collection("mydocuments"),
      or(where("x", "==", 6),
         where("x", "==", 42)
      )
    )

query(db.collection("mydocuments"),
      where("x", "in", [6, 42, 99, 105, 200])
    )

评估查询的限制条件

您的安全规则也可以根据查询的限制条件接受或拒绝查询。 request.query 变量包含查询的 limitoffsetorderBy 属性。例如,如果查询没有将检索到的文档数量上限限制为特定范围,那么您的安全规则可以拒绝该查询:

allow list: if request.query.limit <= 10;

以下规则集演示了如何编写会评估查询的限制条件的安全规则。此示例扩展了上述 stories 规则集,并进行了以下更改:

  • 该规则集将读取规则拆分成了 get 规则和 list 规则。
  • get 规则将针对单个文档的检索限定为公开文档或用户编写的文档。
  • list 规则会应用与 get 规则相同的限制条件,但应用对象为查询。它还会检查查询的限制,并拒绝没有设置数量限制或数量限制大于 10 的查询。
  • 该规则集定义了一个 authorOrPublished() 函数以避免代码重复。
service cloud.firestore {

  match /databases/{database}/documents {

    match /stories/{storyid} {

      // Returns `true` if the requested story is 'published'
      // or the user authored the story
      function authorOrPublished() {
        return resource.data.published == true || request.auth.uid == resource.data.author;
      }

      // Deny any query not limited to 10 or fewer documents
      // Anyone can query published stories
      // Authors can query their unpublished stories
      allow list: if request.query.limit <= 10 &&
                     authorOrPublished();

      // Anyone can retrieve a published story
      // Only a story's author can retrieve an unpublished story
      allow get: if authorOrPublished();

      // Only a story's author can write to a story
      allow write: if request.auth.uid == resource.data.author;
    }

  }
}

集合组查询和安全规则

默认情况下,查询的范围限定为单个集合,并且只从该集合中检索结果。使用集合组查询,您可以从包含具有相同 ID 的所有集合的集合组中检索结果。本部分介绍如何使用安全规则来保护集合组查询。

根据集合组保护和查询文档

在安全规则中,您必须通过为集合组编写规则来明确允许集合组查询:

  1. 确保 rules_version = '2'; 是您的规则集的第一行。集合组查询需要安全规则版本 2 的新递归通配符 {name=**} 行为。
  2. 使用 match /{path=**}/[COLLECTION_ID]/{doc} 为您的集合组编写规则。

例如,假设某个论坛被整理成包含 posts 子集合的 forum 文档:

/forums/{forumid}/posts/{postid}

{
  author: "some_auth_id",
  authorname: "some_username",
  content: "I just read a great story.",
}

在此应用中,我们使帖子可由其所有者修改,并且可由经过身份验证的用户阅读:

service cloud.firestore {
  match /databases/{database}/documents {
    match /forums/{forumid}/posts/{post} {
      // Only authenticated users can read
      allow read: if request.auth != null;
      // Only the post author can write
      allow write: if request.auth != null && request.auth.uid == resource.data.author;
    }
  }
}

任何经过身份验证的用户都可以检索任何单个论坛的帖子:

db.collection("forums/technology/posts").get()

但是,如果您想为当前用户展示他们在所有论坛中的帖子,该怎么办? 您可以使用集合组查询从所有 posts 集合中检索结果:

var user = firebase.auth().currentUser;

db.collectionGroup("posts").where("author", "==", user.uid).get()

在安全规则中,您必须通过为 posts 集合组编写 read 或 list 规则来允许此查询:

rules_version = '2';
service cloud.firestore {

  match /databases/{database}/documents {
    // Authenticated users can query the posts collection group
    // Applies to collection queries, collection group queries, and
    // single document retrievals
    match /{path=**}/posts/{post} {
      allow read: if request.auth != null;
    }
    match /forums/{forumid}/posts/{postid} {
      // Only a post's author can write to a post
      allow write: if request.auth != null && request.auth.uid == resource.data.author;

    }
  }
}

但请注意,这些规则将应用于所有 ID 为 posts 的集合,而无论层次结构如何。例如,这些规则适用于以下所有 posts 集合:

  • /posts/{postid}
  • /forums/{forumid}/posts/{postid}
  • /forums/{forumid}/subforum/{subforumid}/posts/{postid}

根据字段保护集合组查询

与单个集合查询一样,集合组查询也必须满足安全规则设置的限制条件。例如,我们可以像在上面的 stories 示例中一样在每个论坛帖子中添加 published 字段:

/forums/{forumid}/posts/{postid}

{
  author: "some_auth_id",
  authorname: "some_username",
  content: "I just read a great story.",
  published: false
}

然后,我们可以根据 published 状态和帖子 authorposts 集合组编写规则:

rules_version = '2';
service cloud.firestore {

  match /databases/{database}/documents {

    // Returns `true` if the requested post is 'published'
    // or the user authored the post
    function authorOrPublished() {
      return resource.data.published == true || request.auth.uid == resource.data.author;
    }

    match /{path=**}/posts/{post} {

      // Anyone can query published posts
      // Authors can query their unpublished posts
      allow list: if authorOrPublished();

      // Anyone can retrieve a published post
      // Authors can retrieve an unpublished post
      allow get: if authorOrPublished();
    }

    match /forums/{forumid}/posts/{postid} {
      // Only a post's author can write to a post
      allow write: if request.auth.uid == resource.data.author;
    }
  }
}

借助这些规则,Web、Apple、Android 客户端可以执行以下查询:

  • 任何人都可以在论坛中检索已发布的贴子:

    db.collection("forums/technology/posts").where('published', '==', true).get()
    
  • 任何人都可以在所有论坛中检索某个作者发布的帖子:

    db.collectionGroup("posts").where("author", "==", "some_auth_id").where('published', '==', true).get()
    
  • 作者可以在所有论坛中检索其所有已发布和已取消发布的帖子:

    var user = firebase.auth().currentUser;
    
    db.collectionGroup("posts").where("author", "==", user.uid).get()
    

根据集合组和文档路径保护和查询文档

在某些情况下,您可能希望根据文档路径限制集合组查询。如需创建这些限制,您可以使用相同的技术基于某个字段来保护和查询文档。

假设有一个应用,可跟踪每个用户在多个股票和加密货币交易所之间的交易:

/users/{userid}/exchange/{exchangeid}/transactions/{transaction}

{
  amount: 100,
  exchange: 'some_exchange_name',
  timestamp: April 1, 2019 at 12:00:00 PM UTC-7,
  user: "some_auth_id",
}

请注意 user 字段。即使我们可以通过文档路径知道哪个用户拥有 transaction 文档,我们也会在每个 transaction 文档中复制此信息,因为借助此信息可以执行以下两项操作:

  • 编写集合组查询,使其查询范围仅限于在其文档路径中包含特定 /users/{userid} 的文档。例如:

    var user = firebase.auth().currentUser;
    // Return current user's last five transactions across all exchanges
    db.collectionGroup("transactions").where("user", "==", user).orderBy('timestamp').limit(5)
    
  • 对针对 transactions 集合组执行的所有查询强制执行此限制,以使一个用户无法检索另一个用户的 transaction 文档。

我们在安全规则中强制执行此限制,并为 user 字段添加了数据验证:

rules_version = '2';
service cloud.firestore {

  match /databases/{database}/documents {

    match /{path=**}/transactions/{transaction} {
      // Authenticated users can retrieve only their own transactions
      allow read: if resource.data.user == request.auth.uid;
    }

    match /users/{userid}/exchange/{exchangeid}/transactions/{transaction} {
      // Authenticated users can write to their own transactions subcollections
      // Writes must populate the user field with the correct auth id
      allow write: if userid == request.auth.uid && request.data.user == request.auth.uid
    }
  }
}

后续步骤