控制對特定字段的訪問

本頁面以建立安全規則編寫安全規則條件中的概念為基礎,說明如何使用 Cloud Firestore 安全性規則建立規則,允許用戶端對文件中的某些欄位執行操作,但不能對其他欄位執行操作。

有時您可能希望不在文件層級而是在欄位層級控制對文件的變更。

例如,您可能希望允許客戶建立或變更文檔,但不允許他們編輯該文檔中的某些欄位。或者您可能希望強制客戶端建立的任何文件始終包含一組特定的欄位。本指南介紹如何使用 Cloud Firestore 安全規則完成其中一些任務。

僅允許特定字段的讀取訪問

Cloud Firestore 中的讀取是在文件層級執行的。您要么檢索完整的文檔,要么什麼也檢索不到。無法檢索部分文檔。僅使用安全規則來阻止使用者閱讀文件中的特定欄位是不可能的。

如果您希望文件中的某些欄位對某些使用者隱藏,最好的方法是將它們放在單獨的文件中。例如,您可能會考慮在private子集合中建立文檔,如下所示:

/員工/{emp_id}

  name: "Alice Hamilton",
  department: 461,
  start_date: <timestamp>

/員工/{emp_id}/私人/財務

    salary: 80000,
    bonus_mult: 1.25,
    perf_review: 4.2

然後,您可以新增對兩個集合具有不同存取等級的安全性規則。在此範例中,我們使用自訂身份驗證聲明來表示,只有具有等於「 Finance的自訂身份驗證聲明role的使用者才能查看員工的財務資訊。

service cloud.firestore {
  match /databases/{database}/documents {
    // Allow any logged in user to view the public employee data
    match /employees/{emp_id} {
      allow read: if request.resource.auth != null
      // Allow only users with the custom auth claim of "Finance" to view
      // the employee's financial data
      match /private/finances {
        allow read: if request.resource.auth &&
          request.resource.auth.token.role == 'Finance'
      }
    }
  }
}

限製文件建立的字段

Cloud Firestore 是無架構的,這意味著在資料庫級別,文件包含的欄位沒有限制。雖然這種靈活性可以使開發變得更加容易,但有時您會希望確保客戶只能建立包含特定欄位或不包含其他欄位的文件。

您可以透過檢查request.resource.data物件的keys方法來建立這些規則。這是客戶端嘗試在這個新文件中寫入的所有欄位的清單。透過將這組欄位與hasOnly()hasAny()等函數結合,您可以新增邏輯來限制使用者可以新增至 Cloud Firestore 的文件類型。

要求新文件中的特定字段

假設您想要確保在restaurant集合中建立的所有文件至少包含namelocationcity欄位。您可以透過在新文件中的鍵列表上呼叫hasAll()來做到這一點。

service cloud.firestore {
  match /databases/{database}/documents {
    // Allow the user to create a document only if that document contains a name
    // location, and city field
    match /restaurant/{restId} {
      allow create: if request.resource.data.keys().hasAll(['name', 'location', 'city']);
    }
  }
}

這也允許使用其他欄位來建立餐廳,但它確保客戶建立的所有文件至少包含這三個欄位。

禁止新文件中的特定字段

同樣,您可以透過對禁止欄位清單使用hasAny()來阻止客戶端建立包含特定欄位的文件。如果文件包含任何這些字段,則此方法的計算結果為 true,因此您可能想要否定結果以禁止某些字段。

例如,在下列範例中,不允許客戶端建立包含average_scorerating_count欄位的文檔,因為這些欄位將在稍後由伺服器呼叫新增。

service cloud.firestore {
  match /databases/{database}/documents {
    // Allow the user to create a document only if that document does *not*
    // contain an average_score or rating_count field.
    match /restaurant/{restId} {
      allow create: if (!request.resource.data.keys().hasAny(
        ['average_score', 'rating_count']));
    }
  }
}

為新文件建立欄位白名單

您可能想要建立一個僅包含新文件中明確允許的欄位的列表,而不是禁止新文件中的某些欄位。然後,您可以使用hasOnly()函數來確保建立的任何新文件僅包含這些欄位(或這些欄位的子集),而不包含其他欄位。

service cloud.firestore {
  match /databases/{database}/documents {
    // Allow the user to create a document only if that document doesn't contain
    // any fields besides the ones listed below.
    match /restaurant/{restId} {
      allow create: if (request.resource.data.keys().hasOnly(
        ['name', 'location', 'city', 'address', 'hours', 'cuisine']));
    }
  }
}

組合必填字段和可選字段

您可以在安全規則中將hasAllhasOnly操作組合在一起,以要求某些欄位並允許其他欄位。例如,此範例要求所有新文件包含namelocationcity字段,並且可以選擇允許addresshourscuisine字段。

service cloud.firestore {
  match /databases/{database}/documents {
    // Allow the user to create a document only if that document has a name,
    // location, and city field, and optionally address, hours, or cuisine field
    match /restaurant/{restId} {
      allow create: if (request.resource.data.keys().hasAll(['name', 'location', 'city'])) &&
       (request.resource.data.keys().hasOnly(
           ['name', 'location', 'city', 'address', 'hours', 'cuisine']));
    }
  }
}

在現實場景中,您可能希望將此邏輯移至輔助函數中,以避免重複程式碼並更輕鬆地將可選欄位和必填欄位合併到單一清單中,如下所示:

service cloud.firestore {
  match /databases/{database}/documents {
    function verifyFields(required, optional) {
      let allAllowedFields = required.concat(optional);
      return request.resource.data.keys().hasAll(required) &&
        request.resource.data.keys().hasOnly(allAllowedFields);
    }
    match /restaurant/{restId} {
      allow create: if verifyFields(['name', 'location', 'city'],
        ['address', 'hours', 'cuisine']);
    }
  }
}

限制更新字段

常見的安全性做法是只允許客戶端編輯某些字段,而不允許編輯其他字段。您不能僅透過查看上一節中所述的request.resource.data.keys()清單來完成此操作,因為該清單代表更新後查看的完整文檔,因此會包含客戶端未包含的欄位變更。

但是,如果您要使用diff()函數,則可以將request.resource.dataresource.data物件進行比較,該物件表示更新先前資料庫中的文件。這將建立一個mapDiff對象,該對象包含兩個不同地圖之間的所有變更。

透過呼叫此mapDiff 上的affectedKeys()方法,您可以得出編輯中變更的一組欄位。然後,您可以使用hasOnly()hasAny()等函數來確保該集合確實(或不)包含某些項目。

防止某些欄位被更改

透過對affectedKeys() )產生的集合使用hasAny()方法,然後否定結果,您可以拒絕任何嘗試更改您不希望更改的欄位的用戶端請求。

例如,您可能希望允許客戶更新有關餐廳的信息,但不會更改他們的平均得分或評論數量。

service cloud.firestore {
  match /databases/{database}/documents {
    match /restaurant/{restId} {
      // Allow the client to update a document only if that document doesn't
      // change the average_score or rating_count fields
      allow update: if (!request.resource.data.diff(resource.data).affectedKeys()
        .hasAny(['average_score', 'rating_count']));
    }
  }
}

只允許更改某些字段

您也可以使用hasOnly()函數來指定您確實希望更改的欄位列表,而不是指定您不想更改的欄位。這通常被認為更安全,因為預設不允許寫入任何新文件字段,除非您在安全規則中明確允許它們。

例如,您可以建立安全規則,允許客戶端僅更改namelocationcityaddresshourscuisine字段,而不是average_scorerating_count字段。

service cloud.firestore {
  match /databases/{database}/documents {
    match /restaurant/{restId} {
    // Allow a client to update only these 6 fields in a document
      allow update: if (request.resource.data.diff(resource.data).affectedKeys()
        .hasOnly(['name', 'location', 'city', 'address', 'hours', 'cuisine']));
    }
  }
}

這意味著,如果在應用程式的未來迭代中,餐廳文件包含telephone字段,則嘗試編輯該字段將失敗,直到您返回並將該字段添加到安全規則中的hasOnly()列表中。

強制執行欄位類型

Cloud Firestore 無模式的另一個影響是,在資料庫層級沒有強制規定哪些類型的資料可以儲存在特定欄位中。不過,您可以使用is運算子在安全規則中強制執行這一點。

例如,以下安全規則強制評論的score欄位必須是整數, headlinecontentauthor_name欄位必須是字串,而review_date是時間戳。

service cloud.firestore {
  match /databases/{database}/documents {
    match /restaurant/{restId} {
      // Restaurant rules go here...
      match /review/{reviewId} {
        allow create: if (request.resource.data.score is int &&
          request.resource.data.headline is string &&
          request.resource.data.content is string &&
          request.resource.data.author_name is string &&
          request.resource.data.review_date is timestamp
        );
      }
    }
  }
}

is運算子的有效資料型別為boolbytesfloatintlistlatlngnumberpathmapstringtimestampis運算子也支援constraintdurationsetmap_diff資料類型,但由於這些資料類型是由安全規則語言本身產生的,而不是由客戶端產生的,因此在大多數實際應用程式中很少使用它們。

listmap資料類型不支援泛型或類型參數。換句話說,您可以使用安全規則強制某個欄位包含清單或映射,但不能強制某個欄位包含所有整數或所有字串的清單。

同樣,您可以使用安全規則來強制清單或映射中特定條目的類型值(分別使用小括號表示法或鍵名稱),但沒有捷徑來強制映射或清單中所有成員的資料類型一次。

例如,以下規則確保文件中的tags欄位包含清單並且第一個條目是字串。它還確保product欄位包含一個映射,該映射又包含一個字串形式的產品名稱和一個整數形式的數量。

service cloud.firestore {
  match /databases/{database}/documents {
  match /orders/{orderId} {
    allow create: if request.resource.data.tags is list &&
      request.resource.data.tags[0] is string &&
      request.resource.data.product is map &&
      request.resource.data.product.name is string &&
      request.resource.data.product.quantity is int
      }
    }
  }
}

建立和更新文件時都需要強制執行欄位類型。因此,您可能需要考慮建立一個可以在安全規則的建立和更新部分中呼叫的輔助函數。

service cloud.firestore {
  match /databases/{database}/documents {

  function reviewFieldsAreValidTypes(docData) {
     return docData.score is int &&
          docData.headline is string &&
          docData.content is string &&
          docData.author_name is string &&
          docData.review_date is timestamp;
  }

   match /restaurant/{restId} {
      // Restaurant rules go here...
      match /review/{reviewId} {
        allow create: if reviewFieldsAreValidTypes(request.resource.data) &&
          // Other rules may go here
        allow update: if reviewFieldsAreValidTypes(request.resource.data) &&
          // Other rules may go here
      }
    }
  }
}

強制可選字段的類型

請務必記住,在foo不存在的文件上呼叫request.resource.data.foo會導致錯誤,因此進行該呼叫的任何安全規則都會拒絕該請求。您可以透過使用request.resource.data上的get方法來處理這種情況。 get方法可讓您為從映射中檢索的欄位提供預設參數(如果該欄位不存在)。

例如,如果審閱文件還包含一個可選的photo_url字段和一個可選的tags字段,您想要分別驗證它們是字串還是列表,則可以透過將reviewFieldsAreValidTypes函數重寫為如下所示來完成此操作:

  function reviewFieldsAreValidTypes(docData) {
     return docData.score is int &&
          docData.headline is string &&
          docData.content is string &&
          docData.author_name is string &&
          docData.review_date is timestamp &&
          docData.get('photo_url', '') is string &&
          docData.get('tags', []) is list;
  }

這會拒絕存在tags但不是清單的文檔,同時仍允許不包含tags (或photo_url )欄位的文檔。

絕不允許部分寫入

關於 Cloud Firestore 安全性規則的最後一點是,它們要么允許客戶對文件進行更改,要么拒絕整個編輯。您無法建立安全規則來接受對文件中某些欄位的寫入,同時拒絕同一操作中的其他欄位。