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