以安全的方式查詢資料

本頁面會以「建立安全性規則」和「編寫安全性規則的條件」中的概念為基礎,說明 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 驗證,將 author 欄位設為建立文件的使用者 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.published 都是 true。因此,這項查詢符合安全性規則,可讀取資料。

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 規則集,並進行下列變更:

  • 規則集會將讀取規則分成 getlist 的規則。
  • 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} 為集合群組編寫規則。

舉例來說,假設論壇整理成 forum 文件,其中包含 posts 子集合:

/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 集合群組編寫讀取或清單規則,允許這項查詢:

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}

根據欄位保護集合群組查詢

與單一集合查詢一樣,集合群組查詢也必須符合安全性規則設定的限制。舉例來說,我們可以為每個論壇貼文新增 published 欄位,就像上述 stories 範例一樣:

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

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

然後,我們就可以根據 published 狀態和貼文 author,為 posts 集合群組編寫規則:

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

後續步驟