本頁面會以「設定安全規則結構」和「編寫安全規則的條件」中的概念為基礎,說明如何使用 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 只能允許用戶端變更文件,或拒絕整項編輯要求。您無法建立安全性規則,在同一項作業中接受寫入文件中的某些欄位,同時拒絕寫入其他欄位。