本頁面以「結構安全性規則」和「安全性規則的編寫條件」這兩篇文章為基礎,說明如何使用 Cloud Firestore Security Rules 建立規則,允許用戶端對文件的某些欄位執行作業,但其他欄位則否。
有時您可能想控制文件的變更,但不是在文件層級,而是在欄位層級。
舉例來說,您可能想允許客戶建立或變更文件,但不允許他們編輯該文件中的特定欄位。或者,您可能希望強制用戶端建立的所有文件一律包含特定欄位組合。本指南將說明如何使用 Cloud Firestore Security Rules 完成其中部分工作。
只允許讀取特定欄位的存取權
Cloud Firestore 中的讀取作業會在文件層級執行。您可以選擇擷取完整文件,也可以不擷取任何內容。無法擷取部分文件。無法單獨使用安全性規則來防止使用者讀取文件中的特定欄位。
如果您想隱藏文件中的特定欄位,不讓部分使用者存取,最佳做法是將這些欄位放在另一份文件中。例如,您可以考慮在 private
子集合中建立文件,如下所示:
/employees/{emp_id}
name: "Alice Hamilton",
department: 461,
start_date: <timestamp>
/employees/{emp_id}/private/finances
salary: 80000,
bonus_mult: 1.25,
perf_review: 4.2
接著,您可以新增安全性規則,為這兩個集合設定不同的存取層級。在本例中,我們使用自訂驗證權杖表示,只有自訂驗證權杖 role
等於 Finance
的使用者,才能查看員工的財務資訊。
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
集合中建立的所有文件至少包含 name
、location
和 city
欄位。您可以對新文件中的鍵清單呼叫 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_score
或 rating_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']));
}
}
}
結合必填和選填欄位
您可以在安全性規則中結合 hasAll
和 hasOnly
運算子,要求使用者提供部分欄位,並允許使用其他欄位。舉例來說,這個範例要求所有新文件都必須包含 name
、location
和 city
欄位,並可選擇允許 address
、hours
和 cuisine
欄位。
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.data
與 resource.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()
函式,指定要變更的欄位清單,而非指定不想變更的欄位。一般來說,這會被視為更安全的做法,因為系統預設不允許寫入任何新文件欄位,除非您在安全性規則中明確允許。
舉例來說,您可以建立安全性規則,讓用戶端只能變更 name
、location
、city
、address
、hours
和 cuisine
欄位,而非禁止使用 average_score
和 rating_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
欄位必須為整數、headline
、content
和 author_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
運算子的有效資料類型為 bool
、bytes
、float
、int
、list
、latlng
、number
、path
、map
、string
和 timestamp
。is
運算子也支援 constraint
、duration
、set
和 map_diff
資料類型,但由於這些類型是由安全性規則語言本身產生,而非由用戶端產生,因此您在大多數實際應用程式中很少使用這些類型。
list
和 map
資料類型不支援泛型或類型引數。換句話說,您可以使用安全性規則強制特定欄位包含清單或對應,但無法強制欄位包含所有整數或字串的清單。
同樣地,您可以使用安全性規則,為清單或對應表中的特定項目強制類型值 (分別使用方括號符號或鍵名稱),但沒有任何捷徑可一次強制對應表或清單中所有成員的資料類型。
舉例來說,下列規則可確保文件中的 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 Security Rules 的注意事項是,可允許用戶端變更文件,或是拒絕整個編輯項目。您無法建立安全性規則,在同一個作業中允許寫入文件中的某些欄位,同時拒絕其他欄位。