В этом руководстве на основе основных правил безопасности Firebase показано, как добавлять условия в правила безопасности Firebase Realtime Database.
Основным строительным блоком правил безопасности Realtime Database является условие. Условие – это логическое выражение, которое определяет, разрешена или запрещена определенная операция. Для простых правил в качестве условий отлично подходят литералы true и false. Но язык правил безопасности Realtime Database позволяет писать более сложные условия, которые могут:
- Проверьте аутентификацию пользователей
- Сравнение существующих данных с новыми
- Доступ к разным частям базы данных и их сравнение
- Проверка входящих данных
- Используйте структуру входящих запросов для логики безопасности
Как использовать переменные $ для захвата сегментов пути
Вы можете захватывать части пути для чтения или записи, объявляя переменные захвата с префиксом $.
Это подстановочный знак, который сохраняет значение ключа для использования в условиях правил:
{ "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 Authentication, переменная 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"
}
}
}
}Структурирование базы данных для поддержки условий аутентификации
Обычно полезно структурировать базу данных так, чтобы упростить запись Security 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 – единственный тип правил безопасности, которые не применяются каскадно. Если хотя бы одно правило проверки не будет выполнено для какой-либо дочерней записи, вся операция записи будет отклонена.
Кроме того, определения 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];
Swift
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);
REST
# 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];
Swift
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);
REST
# 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)
В правилах безопасности Realtime Database доступны следующие выражения query..
| Выражения правил на основе запросов | ||
|---|---|---|
| Выражение | Тип | Описание |
| query.orderByKey query.orderByPriority query.orderByValue |
Логическое значение | True для запросов, упорядоченных по ключу, приоритету или значению. В противном случае значение равно False. |
| query.orderByChild | string null |
Используйте строку, чтобы указать относительный путь к дочернему узлу. Пример: query.orderByChild === "address/zip". Если запрос не упорядочен по дочернему узлу, это значение равно null.
|
| query.startAt query.endAt query.equalTo |
string number boolean null |
Возвращает границы выполняемого запроса или null, если границы не заданы. |
| query.limitToFirst query.limitToLast |
number null |
Получает ограничение на выполняемый запрос или возвращает значение null, если ограничение не задано. |
Дальнейшие действия
Теперь вы лучше понимаете, как работают условия в Security Rules, и можете:
Узнайте, как работать с основными сценариями использования, а также о рабочем процессе разработки, тестирования и развертывания Security Rules:
- Ознакомьтесь с полным набором предопределенных Security Rules переменных, которые можно использовать для создания условий.
- Создайте правила для распространенных сценариев.
- Чтобы закрепить знания, изучите ситуации, в которых нужно выявлять и избегать небезопасных правил.
- Узнайте, что такое Firebase Local Emulator Suite и как использовать этот набор инструментов для тестирования Security Rules.
- Ознакомьтесь с доступными методами развертывания Security Rules.
Узнайте о функциях Security Rules, которые доступны только в Realtime Database:
- Узнайте, как индексировать Realtime Database.
- Ознакомьтесь с REST API для развертывания Security Rules.