Warunki użytkowania w regułach bezpieczeństwa baz danych w czasie rzeczywistym

Ten przewodnik opiera się na podstawowym przewodniku po języku reguł zabezpieczeń Firebase, aby pokazać, jak dodawać warunki do reguł zabezpieczeń bazy danych czasu rzeczywistego Firebase.

Podstawowym elementem składowym reguł zabezpieczeń bazy danych czasu rzeczywistego jest warunek . Warunek to wyrażenie logiczne, które określa, czy dana operacja powinna być dozwolona, ​​czy zabroniona. W przypadku podstawowych reguł użycie true i false literałów jako warunków działa doskonale. Ale język reguł bezpieczeństwa bazy danych w czasie rzeczywistym umożliwia pisanie bardziej złożonych warunków, które mogą:

  • Sprawdź uwierzytelnianie użytkownika
  • Oceń istniejące dane z nowo przesłanymi danymi
  • Uzyskaj dostęp i porównaj różne części swojej bazy danych
  • Sprawdź poprawność danych przychodzących
  • Użyj struktury przychodzących zapytań do logiki bezpieczeństwa

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

Możesz przechwytywać fragmenty ścieżki do odczytu lub zapisu, deklarując zmienne przechwytywania z przedrostkiem $ . Służy jako dzika karta 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 innych elementów podrzędnych poza title i color . Każdy zapis, który skutkowałby utworzeniem dodatkowych dzieci, 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 uwierzytelniania użytkownika. Na przykład Twoja aplikacja może chcieć zezwalać tylko zalogowanym użytkownikom na zapisywanie danych.

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

Uwierzytelnianie Firebase integruje się z bazą danych czasu rzeczywistego Firebase, aby umożliwić kontrolowanie dostępu do danych dla poszczególnych użytkowników przy użyciu warunków. Po uwierzytelnieniu użytkownika zmienna auth w regułach bezpieczeństwa bazy danych w czasie rzeczywistym zostanie wypełniona informacjami o użytkowniku. Informacje te obejmują ich unikalny identyfikator ( uid ), a także powiązane dane 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ł zabezpieczeń bazy danych czasu rzeczywistego Firebase z informacjami uwierzytelniającymi użytkowników. Łącząc te dwie koncepcje, możesz kontrolować dostęp do danych na podstawie tożsamości użytkownika.

Zmienna auth

Predefiniowana 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 Użyta metoda uwierzytelniania („hasło”, „anonimowy”, „facebook”, „github”, „google” lub „twitter”).
uid Unikalny identyfikator użytkownika, który gwarantuje, że będzie unikalny dla wszystkich dostawców.
znak Zawartość tokena Firebase Auth ID. Więcej informacji można znaleźć w dokumentacji referencyjnej auth.token .

Oto przykładowa reguła, która używa zmiennej auth , aby zapewnić, że każdy użytkownik może pisać tylko w określonej przez niego ścieżce:

{
  "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

Zwykle pomocne jest zorganizowanie bazy danych w sposób, który ułatwia pisanie Reguł. Jednym z typowych wzorców przechowywania danych użytkowników w Bazie danych czasu rzeczywistego jest przechowywanie wszystkich użytkowników w pojedynczym węźle users , którego dzieci są wartościami uid dla każdego użytkownika. Jeśli chciałbyś ograniczyć dostęp do tych danych tak, aby tylko zalogowany użytkownik mógł zobaczyć własne 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 uwierzytelniania

W przypadku aplikacji, które wymagają niestandardowej kontroli dostępu dla różnych użytkowników, Uwierzytelnianie Firebase umożliwia programistom zgłaszanie roszczeń do użytkownika Firebase . Te roszczenia są dostępne w zmiennej auth.token w Twoich regułach. Oto przykład reguł, które wykorzystują 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 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ą istniały, jeśli operacja zapisu się powiedzie. newData reprezentuje scalony wynik zapisu 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 niepustych:

// 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

Dowolne dane mogą służyć jako kryterium reguł. Używając predefiniowanych zmiennych root , data i newData , możemy uzyskać dostęp do dowolnej ścieżki, która istniałaby 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ł nadrzędny nie ma ustawionej flagi readOnly , a w nowo napisanych danych znajduje się element potomny o nazwie foo :

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

Weryfikowanie danych

Wymuszanie struktur danych oraz sprawdzanie poprawności formatu i zawartości danych powinno odbywać się za pomocą 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 tylko daty w formacie RRRR-MM-DD z lat 1900-2099, 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 kaskadują. Jeśli jakakolwiek reguła walidacji nie powiedzie się w dowolnym rekordzie podrzędnym, cała operacja zapisu zostanie odrzucona. Ponadto definicje sprawdzania poprawności są ignorowane podczas usuwania danych (to znaczy, gdy zapisywana nowa wartość to null ).

Może się to wydawać trywialne, ale w rzeczywistości są one ważnymi funkcjami do pisania potężnych reguł zabezpieczeń 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
Uwaga: ten produkt Firebase nie jest dostępny w miejscu docelowym klipu 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 miejscu docelowym klipu 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);
RESZTA
# 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ła 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 miejscu docelowym klipu 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 miejscu docelowym klipu 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);
RESZTA
# 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 między .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 należy zezwolić na usuwanie.

Zasady 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 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 zabezpieczeń oparte na użytkownikach i reguły oparte na zapytaniach w celu ograniczenia dostępu do danych w kolekcji baskets tylko do koszyków zakupów, które posiada aktywny użytkownik:

"baskets": {
  ".read": "auth.uid !== null &&
            query.orderByChild === 'owner' &&
            query.equalTo === auth.uid" // restrict basket access to owner of basket
}

Następujące zapytanie, które zawiera parametry zapytania w regule, powiodłoby 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 również użyć reguł opartych na zapytaniach, aby ograniczyć ilość danych pobieranych przez klienta za pomocą 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)

Następujące query. wyrażenia są dostępne w regułach bezpieczeństwa bazy danych czasu rzeczywistego.

Wyrażenia reguł oparte na zapytaniach
Wyrażenie Rodzaj Opis
zapytanie.zamówienieByKey
zapytanie.orderByPriority
zapytanie.orderByValue
logiczne Prawda dla zapytań uporządkowanych według klucza, priorytetu lub wartości. W przeciwnym razie fałsz.
zapytanie.orderByChild strunowy
zero
Użyj ciągu 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.equalTo
strunowy
numer
logiczne
zero
Pobiera granice wykonywanego zapytania lub zwraca wartość null, jeśli nie ma zestawu ograniczeń.
zapytanie.limitToFirst
zapytanie.limitToLast
numer
zero
Pobiera limit wykonywanego zapytania lub zwraca wartość null, jeśli nie ustawiono limitu.

Następne kroki

Po tym 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ł, które są specyficzne dla Bazy danych czasu rzeczywistego: