Ten przewodnik opiera się na podstawowym przewodniku językowym dotyczącym zasad bezpieczeństwa Firebase, aby pokazać, jak dodać warunki do reguł bezpieczeństwa bazy danych czasu rzeczywistego Firebase.
Podstawowym elementem składowym reguł bezpieczeństwa bazy danych czasu rzeczywistego jest warunek . Warunek to wyrażenie logiczne określające, czy dana operacja powinna być dozwolona, czy zabroniona. W przypadku podstawowych reguł używanie literałów true
i false
jako warunków działa doskonale. Ale język Reguł Bezpieczeństwa Bazy Danych Czasu Rzeczywistego umożliwia pisanie bardziej złożonych warunków, które mogą:
- Sprawdź uwierzytelnianie użytkownika
- Oceń istniejące dane w porównaniu z nowo przesłanymi danymi
- Uzyskaj dostęp i porównaj różne części swojej bazy danych
- Weryfikuj dane przychodzące
- Użyj struktury zapytań przychodzących dla logiki zabezpieczeń
Używanie zmiennych $ do przechwytywania segmentów ścieżki
Możesz przechwycić części ścieżki do odczytu lub zapisu, deklarując zmienne przechwytywania z prefiksem $
. Służy to jako symbol wieloznaczny i przechowuje wartość tego klucza do użycia w warunkach reguł:
{ "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')" } } } } }
Dynamiczne zmienne $
mogą być również używane równolegle ze stałymi nazwami ścieżek. W tym przykładzie używamy zmiennej $other
do zadeklarowania reguły .validate
, która zapewnia, że widget
nie ma elementów podrzędnych innych niż title
i color
. Każdy zapis, który spowodowałby utworzenie dodatkowych elementów podrzędnych, zakończyłby się niepowodzeniem.
{ "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 } } } }
Uwierzytelnianie
Jednym z najczęstszych wzorców reguł bezpieczeństwa jest kontrolowanie dostępu na podstawie stanu uwierzytelnienia użytkownika. Na przykład Twoja aplikacja może zezwolić tylko zalogowanym użytkownikom na zapisywanie danych.
Jeśli Twoja aplikacja korzysta z uwierzytelniania Firebase, zmienna request.auth
zawiera informacje uwierzytelniające dla klienta żądającego danych. Aby uzyskać więcej informacji na temat request.auth
, zobacz dokumentację referencyjną .
Uwierzytelnianie Firebase integruje się z bazą danych czasu rzeczywistego Firebase, umożliwiając kontrolę dostępu do danych dla poszczególnych użytkowników za pomocą warunków. Po uwierzytelnieniu użytkownika zmienna auth
w regułach bezpieczeństwa bazy danych czasu rzeczywistego zostanie wypełniona informacjami o użytkowniku. Informacje te obejmują ich unikalny identyfikator ( uid
), a także dane powiązanego konta, takie jak identyfikator Facebooka lub adres e-mail oraz inne informacje. Jeśli zaimplementujesz niestandardowego dostawcę uwierzytelniania, możesz dodać własne pola do ładunku uwierzytelniania użytkownika.
W tej sekcji wyjaśniono, jak połączyć język reguł bezpieczeństwa bazy danych czasu rzeczywistego Firebase z informacjami uwierzytelniającymi o użytkownikach. Łącząc te dwie koncepcje, możesz kontrolować dostęp do danych na podstawie tożsamości użytkownika.
Zmienna auth
Wstępnie zdefiniowana zmienna auth
w regułach ma wartość null przed przeprowadzeniem uwierzytelnienia.
Gdy użytkownik zostanie uwierzytelniony za pomocą uwierzytelniania Firebase, będzie zawierał następujące atrybuty:
dostawca | Zastosowana metoda uwierzytelniania („hasło”, „anonimowy”, „facebook”, „github”, „google” lub „twitter”). |
uid | Unikalny identyfikator użytkownika, gwarantowany jako unikalny dla wszystkich dostawców. |
znak | Zawartość tokena Firebase Auth ID. Zobacz dokumentację referencyjną dla auth.token , aby uzyskać więcej informacji. |
Oto przykładowa reguła, która wykorzystuje zmienną auth
, aby upewnić się, że każdy użytkownik może zapisywać tylko w ścieżce określonej przez użytkownika:
{ "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" } } } }
Strukturyzacja bazy danych w celu obsługi warunków uwierzytelniania
Zazwyczaj pomocne jest zbudowanie bazy danych w sposób ułatwiający pisanie Reguł. Jednym z powszechnych wzorców przechowywania danych użytkownika w bazie danych czasu rzeczywistego jest przechowywanie wszystkich użytkowników w pojedynczym węźle users
, którego elementami potomnymi są wartości uid
dla każdego użytkownika. Gdybyś chciał ograniczyć dostęp do tych danych, tak aby tylko zalogowany użytkownik mógł zobaczyć swoje własne dane, Twoje reguły wyglądałyby mniej więcej tak.
{ "rules": { "users": { "$uid": { ".read": "auth !== null && auth.uid === $uid" } } } }
Praca z oświadczeniami niestandardowymi uwierzytelniania
W przypadku aplikacji, które wymagają niestandardowej kontroli dostępu dla różnych użytkowników, uwierzytelnianie Firebase umożliwia programistom ustawianie roszczeń wobec użytkownika Firebase . Te roszczenia są dostępne w zmiennej auth.token
w twoich regułach. Oto przykład reguł, które wykorzystują żądanie niestandardowe hasEmergencyTowel
:
{ "rules": { "frood": { // A towel is about the most massively useful thing an interstellar // hitchhiker can have ".read": "auth.token.hasEmergencyTowel === true" } } }
Deweloperzy tworzący własne niestandardowe tokeny uwierzytelniania mogą opcjonalnie dodawać oświadczenia do tych tokenów. Te roszczenia są dostępne w zmiennej auth.token
w twoich regułach.
Istniejące dane a nowe dane
Predefiniowana zmienna data
służy do odwoływania się do danych przed wykonaniem operacji zapisu. I odwrotnie, zmienna newData
zawiera nowe dane, które będą istnieć, jeśli operacja zapisu zakończy się pomyślnie. newData
reprezentuje połączony wynik zapisywanych nowych danych i istniejących danych.
Aby to zilustrować, ta reguła pozwoliłaby nam tworzyć nowe rekordy lub usuwać istniejące, ale nie wprowadzać zmian w istniejących danych innych niż null:
// 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()"
Odwoływanie się do danych w innych ścieżkach
Jako kryterium dla reguł można użyć dowolnych danych. Używając predefiniowanych zmiennych root
, data
i newData
, możemy uzyskać dostęp do dowolnej ścieżki, tak jak istniałaby ona przed lub po zdarzeniu zapisu.
Rozważmy ten przykład, który zezwala na operacje zapisu, o ile wartość węzła /allow_writes/
jest true
, węzeł readOnly
, aw nowo zapisanych danych występuje element potomny o nazwie foo
:
".write": "root.child('allow_writes').val() === true && !data.parent().child('readOnly').exists() && newData.child('foo').exists()"
Walidacja danych
Wymuszanie struktur danych oraz sprawdzanie poprawności formatu i zawartości danych powinno odbywać się przy użyciu reguł .validate
, które są uruchamiane tylko wtedy, gdy reguła .write
pomyślnie udzieli dostępu. Poniżej znajduje się przykładowa definicja reguły .validate
, która dopuszcza tylko daty w formacie RRRR-MM-DD z lat 1900-2099, które są sprawdzane za pomocą wyrażenia regularnego.
".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])$/)"
Reguły .validate
są jedynym rodzajem reguł bezpieczeństwa, które nie są kaskadowane. Jeśli jakakolwiek reguła sprawdzania poprawności zakończy się niepowodzeniem w jakimkolwiek rekordzie podrzędnym, cała operacja zapisu zostanie odrzucona. Ponadto definicje sprawdzania poprawności są ignorowane, gdy dane są usuwane (to znaczy, gdy zapisywana nowa wartość to null
).
Może się to wydawać trywialne, ale w rzeczywistości są to istotne funkcje do pisania potężnych reguł bezpieczeństwa bazy danych czasu rzeczywistego Firebase. Rozważ następujące zasady:
{ "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()" } } } }
Mając na uwadze ten wariant, spójrz na wyniki następujących operacji zapisu:
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);
Cel 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];
Szybki
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);
Jawa
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);
ODPOCZYNEK
# 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
Przyjrzyjmy się teraz tej samej strukturze, ale używając reguł .write
zamiast .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()" } } } }
W tym wariancie powiodłaby się każda z następujących operacji:
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);
Cel 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];
Szybki
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)
Jawa
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);
ODPOCZYNEK
# 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
Ilustruje to różnice między regułami .write
i .validate
. Jak pokazano, wszystkie te reguły powinny być napisane przy użyciu .validate
, z możliwym wyjątkiem reguły newData.hasChildren()
, która zależy od tego, czy usuwanie powinno być dozwolone.
Reguły oparte na zapytaniach
Chociaż nie możesz używać reguł jako filtrów , możesz ograniczyć dostęp do podzbiorów danych, używając parametrów zapytania w swoich regułach. Użyj query.
wyrażenia w swoich regułach, aby przyznać dostęp do odczytu lub zapisu na podstawie parametrów zapytania.
Na przykład poniższa reguła oparta na zapytaniach używa reguł bezpieczeństwa opartych na użytkownikach i reguł opartych na zapytaniach, aby ograniczyć dostęp do danych w kolekcji baskets
tylko do koszyków zakupów, których właścicielem jest aktywny użytkownik:
"baskets": {
".read": "auth.uid !== null &&
query.orderByChild === 'owner' &&
query.equalTo === auth.uid" // restrict basket access to owner of basket
}
Następująca kwerenda, która zawiera parametry kwerendy w regule, powiedzie się:
db.ref("baskets").orderByChild("owner")
.equalTo(auth.currentUser.uid)
.on("value", cb) // Would succeed
Jednak zapytania, które nie zawierają parametrów w regule, zakończą się niepowodzeniem z błędem PermissionDenied
:
db.ref("baskets").on("value", cb) // Would fail with PermissionDenied
Możesz także użyć reguł opartych na zapytaniach, aby ograniczyć ilość danych pobieranych przez klienta w ramach operacji odczytu.
Na przykład poniższa reguła ogranicza dostęp do odczytu tylko do pierwszych 1000 wyników zapytania, uporządkowanych według priorytetu:
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)
Poniższe query.
wyrażenia są dostępne w regułach bezpieczeństwa bazy danych czasu rzeczywistego.
Wyrażenia reguł oparte na zapytaniach | ||
---|---|---|
Wyrażenie | Typ | Opis |
zapytanie.kolejnośćWedług klucza zapytanie.kolejnośćWgPriorytetu zapytanie.kolejnośćWedługWartości | logiczna | Prawda dla zapytań uporządkowanych według klucza, priorytetu lub wartości. Fałsz inaczej. |
zapytanie.zamówienieWgDziecka | strunowy zero | Użyj ciągu, aby przedstawić ścieżkę względną do węzła podrzędnego. Na przykład query.orderByChild === "address/zip" . Jeśli zapytanie nie jest uporządkowane przez węzeł podrzędny, ta wartość jest równa null. |
zapytanie.startAt zapytanie.endAt zapytanie.równaTo | strunowy numer logiczna zero | Pobiera granice wykonywanego zapytania lub zwraca wartość null, jeśli nie ma zestawu powiązań. |
zapytanie.limitDoPierwszego zapytanie.limitToLast | numer zero | Pobiera limit dla wykonywanego zapytania lub zwraca wartość null, jeśli nie ma ustawionego limitu. |
Następne kroki
Po tym omówieniu warunków masz bardziej wyrafinowane zrozumienie zasad i jesteś gotowy do:
Dowiedz się, jak radzić sobie z podstawowymi przypadkami użycia i poznaj przepływ pracy podczas opracowywania, testowania i wdrażania reguł:
- Dowiedz się więcej o pełnym zestawie predefiniowanych zmiennych Reguł, których możesz użyć do tworzenia warunków .
- Napisz reguły, które dotyczą typowych scenariuszy .
- Poszerzaj swoją wiedzę, przeglądając sytuacje, w których musisz wykrywać niebezpieczne reguły i ich unikać .
- Dowiedz się więcej o pakiecie Firebase Local Emulator Suite i o tym, jak możesz go używać do testowania reguł .
- Przejrzyj dostępne metody wdrażania reguł .
Poznaj funkcje reguł specyficzne dla bazy danych czasu rzeczywistego:
- Dowiedz się, jak indeksować bazę danych czasu rzeczywistego .
- Przejrzyj interfejs API REST do wdrażania reguł .