Это руководство основано на изучении основного языкового руководства по правилам безопасности Firebase и показывает, как добавлять условия в правила безопасности базы данных Firebase в реальном времени.
Основным строительным блоком правил безопасности базы данных в реальном времени является условие . Условие — это логическое выражение, которое определяет, следует ли разрешить или запретить конкретную операцию. Для базовых правил использование 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 интегрируется с базой данных Firebase Realtime, что позволяет вам контролировать доступ к данным для каждого пользователя с использованием условий. После аутентификации пользователя переменная auth
в правилах безопасности базы данных реального времени будет заполнена информацией о пользователе. Эта информация включает в себя их уникальный идентификатор ( uid
), а также данные связанной учетной записи, такие как идентификатор Facebook или адрес электронной почты, и другую информацию. Если вы реализуете собственный поставщик аутентификации, вы можете добавить свои собственные поля в полезные данные аутентификации вашего пользователя.
В этом разделе объясняется, как объединить язык правил безопасности базы данных Firebase Realtime с информацией аутентификации ваших пользователей. Объединив эти две концепции, вы можете контролировать доступ к данным на основе личности пользователя.
Переменная auth
Предопределенная переменная auth
в правилах имеет значение null до того, как произойдет аутентификация.
После аутентификации пользователя с помощью Firebase Authentication он будет содержать следующие атрибуты:
поставщик | Используемый метод аутентификации («пароль», «анонимный», «facebook», «github», «google» или «twitter»). |
жидкость | Уникальный идентификатор пользователя, который гарантированно будет уникальным для всех поставщиков. |
жетон | Содержимое токена идентификатора аутентификации Firebase. Дополнительную информацию см. в справочной документации по 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" } } } }
Структурирование базы данных для поддержки условий аутентификации
Обычно полезно структурировать базу данных таким образом, чтобы облегчить написание правил. Одним из распространенных шаблонов хранения пользовательских данных в базе данных реального времени является хранение всех ваших пользователей в одном узле users
, дочерними элементами которого являются значения uid
для каждого пользователя. Если вы хотите ограничить доступ к этим данным, чтобы только вошедший в систему пользователь мог видеть свои собственные данные, ваши правила будут выглядеть примерно так.
{ "rules": { "users": { "$uid": { ".read": "auth !== null && auth.uid === $uid" } } } }
Работа с пользовательскими утверждениями проверки подлинности
Для приложений, требующих индивидуального контроля доступа для разных пользователей, проверка подлинности Firebase позволяет разработчикам устанавливать требования для пользователя 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);
Цель-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);
Цель-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.
выражения доступны в правилах безопасности базы данных реального времени.
Выражения правил на основе запросов | ||
---|---|---|
Выражение | Тип | Описание |
query.orderByKey query.orderByPriority query.orderByValue | логическое значение | Верно для запросов, упорядоченных по ключу, приоритету или значению. В противном случае неверно. |
query.orderByChild | нить нулевой | Используйте строку для представления относительного пути к дочернему узлу. Например, query.orderByChild === "address/zip" . Если запрос не упорядочен дочерним узлом, это значение равно нулю. |
запрос.startAt запрос.endAt запрос.equalTo | нить число логическое значение нулевой | Извлекает границы выполняющегося запроса или возвращает значение NULL, если набор границ отсутствует. |
query.limitToFirst query.limitToLast | число нулевой | Получает ограничение на выполнение запроса или возвращает значение NULL, если ограничение не установлено. |
Следующие шаги
После обсуждения условий вы получите более глубокое понимание Правил и будете готовы:
Узнайте, как обрабатывать основные сценарии использования, а также изучите рабочий процесс разработки, тестирования и развертывания правил:
- Узнайте о полном наборе предопределенных переменных правил, которые можно использовать для создания условий .
- Напишите правила, которые касаются распространенных сценариев .
- Закрепите свои знания, рассматривая ситуации, в которых вам необходимо обнаружить и избежать небезопасных правил .
- Узнайте о пакете локального эмулятора Firebase и о том, как его можно использовать для тестирования правил .
- Ознакомьтесь с методами, доступными для развертывания Rules .
Изучите функции правил, специфичные для базы данных реального времени:
- Узнайте, как индексировать базу данных реального времени .
- Ознакомьтесь с REST API для развертывания Rules .