Użyj warunków w Regułach bezpieczeństwa bazy danych w czasie rzeczywistym

Ten przewodnik opiera się na podstawowym przewodniku językowym Firebase Security Rules, aby pokazać, jak dodać warunki do reguł bezpieczeństwa bazy danych Firebase Realtime.

Podstawowym elementem 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 zasad używanie literałów true i false jako warunków sprawdza się doskonale. Jednak język Reguły bezpieczeństwa bazy danych czasu rzeczywistego umożliwia pisanie bardziej złożonych warunków, które mogą:

  • Sprawdź uwierzytelnienie 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
  • Zweryfikuj przychodzące dane
  • Użyj struktury przychodzących zapytań dla logiki bezpieczeństwa

Używanie zmiennych $ do przechwytywania segmentów ścieżki

Możesz przechwycić fragmenty ś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 gwarantuje, że widget nie będzie miał żadnych dzieci poza title i color . Jakikolwiek zapis, który spowodowałby utworzenie dodatkowych dzieci, zakończy 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 w oparciu o stan uwierzytelnienia użytkownika. Na przykład Twoja aplikacja może chcieć zezwalać na zapisywanie danych tylko zalogowanym użytkownikom.

Jeśli Twoja aplikacja korzysta z uwierzytelniania Firebase, zmienna request.auth zawiera informacje uwierzytelniające klienta żądającego danych. Więcej informacji na temat request.auth można znaleźć w dokumentacji referencyjnej .

Uwierzytelnianie Firebase integruje się z bazą danych Firebase Realtime Database, aby umożliwić kontrolę dostępu do danych dla poszczególnych użytkowników za pomocą warunków. Po uwierzytelnieniu użytkownika zmienna auth w regułach zabezpieczeń 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 Facebook lub adres e-mail, a także 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 Firebase Realtime z informacjami uwierzytelniającymi dotyczącymi użytkowników. Łącząc te dwie koncepcje, można kontrolować dostęp do danych w oparciu o tożsamość użytkownika.

Zmienna auth

Predefiniowana zmienna auth w regułach ma wartość null przed wykonaniem uwierzytelnienia.

Po uwierzytelnieniu użytkownika za pomocą uwierzytelniania Firebase będzie on zawierał następujące atrybuty:

dostawca Stosowana metoda uwierzytelniania („hasło”, „anonimowy”, „facebook”, „github”, „google” lub „twitter”).
uid Unikalny identyfikator użytkownika, który gwarantuje unikalność u wszystkich dostawców.
znak Zawartość tokena identyfikatora uwierzytelniania Firebase. Więcej szczegółów można znaleźć w dokumentacji referencyjnej pliku auth.token .

Oto przykładowa reguła korzystająca ze zmiennej auth w celu zapewnienia, że ​​każdy użytkownik może zapisywać tylko w ścieżce specyficznej dla 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"
      }
    }
  }
}

Struktura bazy danych w celu obsługi warunków uwierzytelniania

Zwykle pomocne jest zorganizowanie bazy danych w sposób ułatwiający pisanie Reguł. Jednym z powszechnych wzorców przechowywania danych użytkowników w Bazie danych czasu rzeczywistego jest przechowywanie wszystkich użytkowników w jednym węźle users , którego elementy podrzędne są wartościami uid każdego użytkownika. Jeśli chciałbyś ograniczyć dostęp do tych danych tak, aby tylko zalogowany użytkownik mógł widzieć swoje dane, Twoje reguły wyglądałyby mniej więcej tak.

{
  "rules": {
    "users": {
      "$uid": {
        ".read": "auth !== null && auth.uid === $uid"
      }
    }
  }
}

Praca z niestandardowymi oświadczeniami uwierzytelniającymi

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 oświadczenia są dostępne w zmiennej auth.token w regułach. Oto przykład reguł wykorzystujących niestandardowe oświadczenie 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 oświadczenia są dostępne w zmiennej auth.token w 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ą istniały, jeśli operacja zapisu zakończy się pomyślnie. newData reprezentuje połączony wynik zapisywania 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 reguł można zastosować dowolne dane. Używając predefiniowanych zmiennych root , data i newData , możemy uzyskać dostęp do dowolnej ścieżki, jaka istniałaby przed lub po zdarzeniu zapisu.

Rozważmy ten przykład, który pozwala na operacje zapisu, o ile wartość węzła /allow_writes/ jest true , węzeł nadrzędny nie ma ustawionej flagi readOnly , a w nowo zapisanych danych znajduje się element podrzędny o nazwie foo :

".write": "root.child('allow_writes').val() === true &&
          !data.parent().child('readOnly').exists() &&
          newData.child('foo').exists()"

Walidacja danych

Egzekwowanie struktur danych oraz sprawdzanie formatu i zawartości danych powinno odbywać się przy użyciu reguł .validate , które są uruchamiane dopiero po pomyślnym przyznaniu dostępu przez regułę .write . Poniżej znajduje się przykładowa definicja reguły .validate , która dopuszcza daty w formacie RRRR-MM-DD z lat 1900-2099, co jest 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 typem reguł bezpieczeństwa, które nie działają kaskadowo. Jeśli jakakolwiek reguła sprawdzania poprawności nie powiedzie się w jakimkolwiek rekordzie podrzędnym, cała operacja zapisu zostanie odrzucona. Ponadto definicje sprawdzania poprawności są ignorowane w przypadku usunięcia danych (to znaczy, gdy zapisywana nowa wartość ma wartość null ).

Mogą się one wydawać trywialne, ale w rzeczywistości są to istotne funkcje ułatwiające pisanie potężnych reguł bezpieczeństwa bazy danych Firebase Realtime. 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
Uwaga: ten produkt Firebase nie jest dostępny w docelowym klipie aplikacji.
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
Uwaga: ten produkt Firebase nie jest dostępny w docelowym klipie aplikacji.
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ę dowolna 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
Uwaga: ten produkt Firebase nie jest dostępny w docelowym klipie aplikacji.
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
Uwaga: ten produkt Firebase nie jest dostępny w docelowym klipie aplikacji.
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

To ilustruje różnice pomię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 powinno być dozwolone usuwanie.

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 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 wykorzystuje reguły bezpieczeństwa oparte na użytkownikach i reguły oparte 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
}

Poniższe zapytanie, które zawiera parametry zapytania 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, nie powiodą się z powodu błędu 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 zabezpieczeń bazy danych czasu rzeczywistego.

Wyrażenia reguł oparte na zapytaniach
Wyrażenie Typ Opis
zapytanie.zamówienieByKey
zapytanie.orderByPriority
zapytanie.zamówienieWedługWartości
wartość logiczna To prawda w przypadku zapytań uporządkowanych według klucza, priorytetu lub wartości. Inaczej nieprawda.
zapytanie.zamówieniePrzezDziecko strunowy
zero
Użyj ciągu znaków do reprezentowania ścieżki względnej 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ść ma wartość null.
zapytanie.startAt
zapytanie.endAt
zapytanie.równeTo
strunowy
numer
wartość logiczna
zero
Pobiera granice wykonującego zapytania lub zwraca wartość null, jeśli nie ma zestawu powiązań.
zapytanie.limitToFirst
zapytanie.limitToLast
numer
zero
Pobiera limit wykonywanego zapytania lub zwraca wartość null, jeśli nie ustawiono żadnego limitu.

Następne kroki

Po omówieniu warunków masz bardziej wyrafinowane zrozumienie Reguł 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ł:

Poznaj funkcje reguł specyficzne dla bazy danych Realtime: