Это руководство основано на руководстве по изучению основного языка правил безопасности Firebase и показывает, как добавлять условия в правила безопасности базы данных Firebase Realtime.
Основным строительным блоком правил безопасности баз данных в реальном времени является условие . Условие — это логическое выражение, которое определяет, следует ли разрешить или запретить конкретную операцию. Для базовых правил использование литералов true
и false
в качестве условий работает идеально. Однако язык правил безопасности баз данных в реальном времени позволяет создавать более сложные условия, которые могут:
- Проверьте аутентификацию пользователя
- Оцените существующие данные по сравнению с вновь представленными данными
- Доступ к различным частям вашей базы данных и сравнение их
- Проверка входящих данных
- Использовать структуру входящих запросов для логики безопасности
Использование $-переменных для захвата сегментов пути
Вы можете захватить части пути для чтения или записи, объявив переменные захвата с префиксом $
. Это служит подстановочным знаком и сохраняет значение ключа для использования в условиях правил:
{ "rules": { "rooms": { // this rule applies to any child of /rooms/, the key for each room id // is stored inside $room_id variable for reference "$room_id": { "topic": { // the room's topic can be changed if the room id has "public" in it ".write": "$room_id.contains('public')" } } } } }
Динамические переменные $
также можно использовать параллельно с константными путями. В этом примере мы используем переменную $other
для объявления правила .validate
, которое гарантирует, что у widget
нет дочерних элементов, кроме title
и color
. Любая запись, которая приведет к созданию дополнительных дочерних элементов, завершится ошибкой.
{ "rules": { "widget": { // a widget can have a title or color attribute "title": { ".validate": true }, "color": { ".validate": true }, // but no other child paths are allowed // in this case, $other means any key excluding "title" and "color" "$other": { ".validate": false } } } }
Аутентификация
Один из наиболее распространённых шаблонов правил безопасности — управление доступом на основе состояния аутентификации пользователя. Например, ваше приложение может разрешить запись данных только вошедшим в систему пользователям.
Если ваше приложение использует аутентификацию Firebase, переменная request.auth
содержит информацию об аутентификации клиента, запрашивающего данные. Подробнее о request.auth
см. в справочной документации .
Firebase Authentication интегрируется с Firebase Realtime Database , позволяя контролировать доступ к данным для каждого пользователя с помощью условий. После аутентификации пользователя переменная auth
в правилах безопасности Realtime Database будет заполнена информацией о пользователе. Эта информация включает в себя уникальный идентификатор ( uid
), а также данные связанной учётной записи, такие как идентификатор Facebook или адрес электронной почты, и другие данные. При реализации собственного провайдера аутентификации вы можете добавлять собственные поля в полезную нагрузку аутентификации пользователя.
В этом разделе объясняется, как объединить язык правил безопасности Firebase Realtime Database с информацией об аутентификации пользователей. Объединив эти две концепции, вы сможете контролировать доступ к данным на основе личности пользователя.
Переменная auth
Предопределенная в правилах переменная auth
имеет значение null до выполнения аутентификации.
После аутентификации пользователя с помощью Firebase Authentication он будет содержать следующие атрибуты:
поставщик | Используемый метод аутентификации («пароль», «анонимный», «facebook», «github», «google» или «twitter»). |
uid | Уникальный идентификатор пользователя, гарантированно уникальный для всех провайдеров. |
токен | Содержимое токена Firebase Auth ID. Подробнее см. в справочной документации по auth.token . |
Вот пример правила, которое использует переменную auth
, чтобы гарантировать, что каждый пользователь может записывать только по указанному для него пути:
{ "rules": { "users": { "$user_id": { // grants write access to the owner of this user account // whose uid must exactly match the key ($user_id) ".write": "$user_id === auth.uid" } } } }
Структурирование базы данных для поддержки условий аутентификации
Обычно полезно структурировать базу данных таким образом, чтобы упростить написание Rules . Один из распространённых шаблонов хранения данных пользователей в Realtime Database — хранить всех пользователей в одном узле, дочерними элементами которого являются значения uid
каждого users
. Если вы хотите ограничить доступ к этим данным так, чтобы только вошедший в систему пользователь мог видеть свои данные, ваши правила будут выглядеть примерно так.
{ "rules": { "users": { "$uid": { ".read": "auth !== null && auth.uid === $uid" } } } }
Работа с пользовательскими утверждениями аутентификации
Для приложений, требующих индивидуального управления доступом для разных пользователей, Firebase Authentication позволяет разработчикам устанавливать требования для пользователя Firebase . Эти требования доступны в переменной auth.token
в ваших правилах. Вот пример правил, использующих настраиваемое требование hasEmergencyTowel
:
{ "rules": { "frood": { // A towel is about the most massively useful thing an interstellar // hitchhiker can have ".read": "auth.token.hasEmergencyTowel === true" } } }
Разработчики, создающие собственные токены аутентификации, могут при необходимости добавлять к ним утверждения. Эти утверждения доступны в переменной auth.token
в ваших правилах.
Существующие данные против новых данных
Предопределенная переменная data
используется для ссылки на данные до выполнения операции записи. Переменная newData
, наоборот, содержит новые данные, которые будут доступны после успешной записи. newData
представляет собой результат объединения новых записываемых и существующих данных.
Для иллюстрации это правило позволит нам создавать новые записи или удалять существующие, но не вносить изменения в существующие ненулевые данные:
// we can write as long as old data or new data does not exist // in other words, if this is a delete or a create, but not an update ".write": "!data.exists() || !newData.exists()"
Ссылки на данные по другим путям
Любые данные могут быть использованы в качестве критерия для правил. Используя предопределённые переменные root
, data
и newData
, мы можем получить доступ к любому пути, который существовал бы до или после события записи.
Рассмотрим этот пример, который разрешает операции записи до тех пор, пока значение узла /allow_writes/
равно true
, родительский узел не имеет установленного флага readOnly
и в новых записанных данных есть дочерний узел с именем foo
:
".write": "root.child('allow_writes').val() === true && !data.parent().child('readOnly').exists() && newData.child('foo').exists()"
Проверка данных
Обеспечение структур данных, а также проверка формата и содержимого данных должны осуществляться с помощью правил .validate
, которые выполняются только после успешного предоставления доступа правилом .write
. Ниже приведён пример определения правила .validate
, которое допускает только даты в формате ГГГГ-ММ-ДД в диапазоне 1900-2099 годов, что проверяется с помощью регулярного выражения.
".validate": "newData.isString() && newData.val().matches(/^(19|20)[0-9][0-9][-\\/. ](0[1-9]|1[012])[-\\/. ](0[1-9]|[12][0-9]|3[01])$/)"
Правила .validate
— единственный тип правил безопасности, которые не каскадируются. Если какое-либо правило валидации не срабатывает для любой дочерней записи, вся операция записи будет отклонена. Кроме того, определения валидации игнорируются при удалении данных (то есть когда новое записываемое значение равно null
).
Эти моменты могут показаться незначительными, но на самом деле они важны для написания эффективных правил безопасности базы данных Firebase Realtime. Обратите внимание на следующие правила:
{ "rules": { // write is allowed for all paths ".write": true, "widget": { // a valid widget must have attributes "color" and "size" // allows deleting widgets (since .validate is not applied to delete rules) ".validate": "newData.hasChildren(['color', 'size'])", "size": { // the value of "size" must be a number between 0 and 99 ".validate": "newData.isNumber() && newData.val() >= 0 && newData.val() <= 99" }, "color": { // the value of "color" must exist as a key in our mythical // /valid_colors/ index ".validate": "root.child('valid_colors/' + newData.val()).exists()" } } } }
Имея в виду этот вариант, рассмотрим результаты следующих операций записи:
JavaScript
var ref = db.ref("/widget"); // PERMISSION_DENIED: does not have children color and size ref.set('foo'); // PERMISSION DENIED: does not have child color ref.set({size: 22}); // PERMISSION_DENIED: size is not a number ref.set({ size: 'foo', color: 'red' }); // SUCCESS (assuming 'blue' appears in our colors list) ref.set({ size: 21, color: 'blue'}); // If the record already exists and has a color, this will // succeed, otherwise it will fail since newData.hasChildren(['color', 'size']) // will fail to validate ref.child('size').set(99);
Objective-C
FIRDatabaseReference *ref = [[[FIRDatabase database] reference] child: @"widget"]; // PERMISSION_DENIED: does not have children color and size [ref setValue: @"foo"]; // PERMISSION DENIED: does not have child color [ref setValue: @{ @"size": @"foo" }]; // PERMISSION_DENIED: size is not a number [ref setValue: @{ @"size": @"foo", @"color": @"red" }]; // SUCCESS (assuming 'blue' appears in our colors list) [ref setValue: @{ @"size": @21, @"color": @"blue" }]; // If the record already exists and has a color, this will // succeed, otherwise it will fail since newData.hasChildren(['color', 'size']) // will fail to validate [[ref child:@"size"] setValue: @99];
Быстрый
var ref = FIRDatabase.database().reference().child("widget") // PERMISSION_DENIED: does not have children color and size ref.setValue("foo") // PERMISSION DENIED: does not have child color ref.setValue(["size": "foo"]) // PERMISSION_DENIED: size is not a number ref.setValue(["size": "foo", "color": "red"]) // SUCCESS (assuming 'blue' appears in our colors list) ref.setValue(["size": 21, "color": "blue"]) // If the record already exists and has a color, this will // succeed, otherwise it will fail since newData.hasChildren(['color', 'size']) // will fail to validate ref.child("size").setValue(99);
Ява
FirebaseDatabase database = FirebaseDatabase.getInstance(); DatabaseReference ref = database.getReference("widget"); // PERMISSION_DENIED: does not have children color and size ref.setValue("foo"); // PERMISSION DENIED: does not have child color ref.child("size").setValue(22); // PERMISSION_DENIED: size is not a number Map<String,Object> map = new HashMap<String, Object>(); map.put("size","foo"); map.put("color","red"); ref.setValue(map); // SUCCESS (assuming 'blue' appears in our colors list) map = new HashMap<String, Object>(); map.put("size", 21); map.put("color","blue"); ref.setValue(map); // If the record already exists and has a color, this will // succeed, otherwise it will fail since newData.hasChildren(['color', 'size']) // will fail to validate ref.child("size").setValue(99);
ОТДЫХ
# PERMISSION_DENIED: does not have children color and size curl -X PUT -d 'foo' \ https://docs-examples.firebaseio.com/rest/securing-data/example.json # PERMISSION DENIED: does not have child color curl -X PUT -d '{"size": 22}' \ https://docs-examples.firebaseio.com/rest/securing-data/example.json # PERMISSION_DENIED: size is not a number curl -X PUT -d '{"size": "foo", "color": "red"}' \ https://docs-examples.firebaseio.com/rest/securing-data/example.json # SUCCESS (assuming 'blue' appears in our colors list) curl -X PUT -d '{"size": 21, "color": "blue"}' \ https://docs-examples.firebaseio.com/rest/securing-data/example.json # If the record already exists and has a color, this will # succeed, otherwise it will fail since newData.hasChildren(['color', 'size']) # will fail to validate curl -X PUT -d '99' \ https://docs-examples.firebaseio.com/rest/securing-data/example/size.json
Теперь давайте рассмотрим ту же структуру, но с использованием правил .write
вместо .validate
:
{ "rules": { // this variant will NOT allow deleting records (since .write would be disallowed) "widget": { // a widget must have 'color' and 'size' in order to be written to this path ".write": "newData.hasChildren(['color', 'size'])", "size": { // the value of "size" must be a number between 0 and 99, ONLY IF WE WRITE DIRECTLY TO SIZE ".write": "newData.isNumber() && newData.val() >= 0 && newData.val() <= 99" }, "color": { // the value of "color" must exist as a key in our mythical valid_colors/ index // BUT ONLY IF WE WRITE DIRECTLY TO COLOR ".write": "root.child('valid_colors/'+newData.val()).exists()" } } } }
В этом варианте любая из следующих операций будет выполнена успешно:
JavaScript
var ref = new Firebase(URL + "/widget"); // ALLOWED? Even though size is invalid, widget has children color and size, // so write is allowed and the .write rule under color is ignored ref.set({size: 99999, color: 'red'}); // ALLOWED? Works even if widget does not exist, allowing us to create a widget // which is invalid and does not have a valid color. // (allowed by the write rule under "color") ref.child('size').set(99);
Objective-C
Firebase *ref = [[Firebase alloc] initWithUrl:URL]; // ALLOWED? Even though size is invalid, widget has children color and size, // so write is allowed and the .write rule under color is ignored [ref setValue: @{ @"size": @9999, @"color": @"red" }]; // ALLOWED? Works even if widget does not exist, allowing us to create a widget // which is invalid and does not have a valid color. // (allowed by the write rule under "color") [[ref childByAppendingPath:@"size"] setValue: @99];
Быстрый
var ref = Firebase(url:URL) // ALLOWED? Even though size is invalid, widget has children color and size, // so write is allowed and the .write rule under color is ignored ref.setValue(["size": 9999, "color": "red"]) // ALLOWED? Works even if widget does not exist, allowing us to create a widget // which is invalid and does not have a valid color. // (allowed by the write rule under "color") ref.childByAppendingPath("size").setValue(99)
Ява
Firebase ref = new Firebase(URL + "/widget"); // ALLOWED? Even though size is invalid, widget has children color and size, // so write is allowed and the .write rule under color is ignored Map<String,Object> map = new HashMap<String, Object>(); map.put("size", 99999); map.put("color", "red"); ref.setValue(map); // ALLOWED? Works even if widget does not exist, allowing us to create a widget // which is invalid and does not have a valid color. // (allowed by the write rule under "color") ref.child("size").setValue(99);
ОТДЫХ
# ALLOWED? Even though size is invalid, widget has children color and size, # so write is allowed and the .write rule under color is ignored curl -X PUT -d '{size: 99999, color: "red"}' \ https://docs-examples.firebaseio.com/rest/securing-data/example.json # ALLOWED? Works even if widget does not exist, allowing us to create a widget # which is invalid and does not have a valid color. # (allowed by the write rule under "color") curl -X PUT -d '99' \ https://docs-examples.firebaseio.com/rest/securing-data/example/size.json
Это иллюстрирует различия между правилами .write
и .validate
. Как показано, все эти правила следует записывать с использованием .validate
, за возможным исключением правила newData.hasChildren()
, которое зависит от того, разрешено ли удаление.
Правила на основе запросов
Хотя правила нельзя использовать в качестве фильтров , вы можете ограничить доступ к подмножествам данных, используя параметры запроса в правилах. Используйте выражения query.
в правилах, чтобы предоставить доступ на чтение или запись на основе параметров запроса.
Например, следующее правило на основе запроса использует правила безопасности на основе пользователя и правила на основе запроса, чтобы ограничить доступ к данным в коллекции baskets
только теми корзинами покупок, которыми владеет активный пользователь:
"baskets": {
".read": "auth.uid !== null &&
query.orderByChild === 'owner' &&
query.equalTo === auth.uid" // restrict basket access to owner of basket
}
Следующий запрос, включающий параметры запроса в правиле, будет выполнен успешно:
db.ref("baskets").orderByChild("owner")
.equalTo(auth.currentUser.uid)
.on("value", cb) // Would succeed
Однако запросы, не включающие параметры в правило, будут завершаться ошибкой PermissionDenied
:
db.ref("baskets").on("value", cb) // Would fail with PermissionDenied
Вы также можете использовать правила на основе запросов, чтобы ограничить объем данных, загружаемых клиентом посредством операций чтения.
Например, следующее правило ограничивает доступ для чтения только первыми 1000 результатами запроса в порядке приоритета:
messages: {
".read": "query.orderByKey &&
query.limitToFirst <= 1000"
}
// Example queries:
db.ref("messages").on("value", cb) // Would fail with PermissionDenied
db.ref("messages").limitToFirst(1000)
.on("value", cb) // Would succeed (default order by key)
Следующие выражения query.
доступны в правилах безопасности базы данных в реальном времени.
Выражения правил на основе запросов | ||
---|---|---|
Выражение | Тип | Описание |
запрос.orderByKey запрос.orderByPriority запрос.orderByValue | булев | True для запросов, упорядоченных по ключу, приоритету или значению. False в противном случае. |
запрос.orderByChild | нить нулевой | Используйте строку для указания относительного пути к дочернему узлу. Например, query.orderByChild === "address/zip" . Если запрос не упорядочен по дочернему узлу, это значение равно NULL. |
query.startAt query.endAt запрос.равно | нить число булев нулевой | Извлекает границы выполняемого запроса или возвращает null, если набор границ не установлен. |
query.limitToFirst query.limitToLast | число нулевой | Возвращает ограничение на выполняемый запрос или возвращает значение null, если ограничение не установлено. |
Следующие шаги
После этого обсуждения условий вы получили более глубокое понимание Rules и готовы:
Узнайте, как обрабатывать основные варианты использования, а также изучите рабочий процесс разработки, тестирования и развертывания Rules :
- Узнайте о полном наборе предопределенных переменных Rules , которые можно использовать для создания условий .
- Напишите правила, которые учитывают распространенные сценарии .
- Расширяйте свои знания, рассматривая ситуации, в которых необходимо выявлять и избегать ненадежных правил .
- Узнайте о Firebase Local Emulator Suite и о том, как его можно использовать для тестирования Rules .
- Ознакомьтесь с доступными методами развертывания Rules .
Изучите функции Rules , специфичные для Realtime Database :
- Узнайте, как индексировать Realtime Database .
- Ознакомьтесь с REST API для развертывания Rules .