控管特定欄位的存取權
本頁內容以「建立安全性規則」和「為安全性規則編寫條件」的概念為基礎,說明如何使用 Firestore 安全性規則建立規則,允許用戶端對文件中的某些欄位執行作業,但不能對其他欄位執行作業。
有時您可能想在欄位層級控管文件變更,而非文件層級。
舉例來說,您可能想允許客戶建立或變更文件,但不允許他們編輯文件中的特定欄位。或者,您可能希望強制規定客戶建立的任何文件都必須包含特定欄位組合。本指南說明如何使用 Firestore 安全性規則完成部分工作。
僅允許特定欄位的讀取權限
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'
}
}
}
}
限制文件建立作業的欄位
Firestore 沒有結構定義,也就是說,文件包含的欄位不受資料庫層級限制。雖然這種彈性可簡化開發作業,但有時您會希望確保用戶端只能建立包含特定欄位的文件,或不包含其他欄位。
您可以檢查 request.resource.data
物件的 keys
方法,建立這些規則。這是用戶端嘗試在這個新文件中寫入的所有欄位清單。將這組欄位與 hasOnly()
或 hasAny()
等函式結合,即可加入相關邏輯,限制使用者可新增至 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()
清單中加入該欄位為止。
強制執行欄位類型
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
) 欄位的文件。
一律不允許部分寫入
最後,請注意 Firestore 安全性規則只會允許用戶端變更文件,或拒絕整項編輯作業。您無法建立安全性規則,在同一項作業中接受寫入文件中的某些欄位,同時拒絕寫入其他欄位。