Данное руководство основано на руководстве по изучению основного языка правил безопасности Firebase и показывает, как добавлять условия в правила безопасности вашей базы данных Firebase Realtime Database.
Основным элементом правил безопасности баз данных в реальном времени является условие . Условие — это логическое выражение, определяющее, разрешена или запрещена конкретная операция. Для простых правил вполне подойдут литералы 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, он будет обладать следующими атрибутами:
| поставщик | Используемый метод аутентификации ("password", "anonymous", "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 — это хранение всех пользователей в одном узле users , дочерними элементами которого являются значения uid для каждого пользователя. Если вы хотите ограничить доступ к этим данным таким образом, чтобы только авторизованный пользователь мог видеть свои собственные данные, ваши правила будут выглядеть примерно так.
{
"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 Database. Рассмотрим следующие правила:
{
"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);
Java
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)
Java
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 | логический | Значение true для запросов, упорядоченных по ключу, приоритету или значению. В противном случае — false. |
| query.orderByChild | нить нулевой | Для представления относительного пути к дочернему узлу используйте строку. Например, query.orderByChild === "address/zip" . Если запрос не упорядочен по дочернему узлу, это значение будет равно null. |
| запрос.начало запрос.конецВ query.equalTo | нить число логический нулевой | Получает границы выполняемого запроса или возвращает null, если границы не заданы. |
| query.limitToFirst query.limitToLast | число нулевой | Получает лимит для выполняемого запроса или возвращает null, если лимит не установлен. |
Следующие шаги
После обсуждения условий у вас сформировалось более глубокое понимание Rules , и вы готовы:
Узнайте, как обрабатывать основные сценарии использования, и освойте рабочий процесс разработки, тестирования и развертывания Rules :
- Узнайте о полном наборе предопределенных переменных Rules которые можно использовать для построения условий .
- Разработайте правила, учитывающие типичные сценарии .
- Расширьте свои знания, проанализировав ситуации, в которых необходимо выявлять и избегать небезопасных правил .
- Узнайте о наборе инструментов Firebase Local Emulator Suite и о том, как его можно использовать для тестирования Rules .
- Ознакомьтесь с доступными методами развертывания Rules .
Изучите особенности Rules , специфичные для Realtime Database :
- Узнайте, как индексировать вашу Realtime Database .
- Ознакомьтесь с REST API для развертывания Rules .