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

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ł nadrzędny nie ma ustawionej flagi tylko do 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 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
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ę 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
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

Ilustruje to 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 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 Rodzaj 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ł:

Poznaj funkcje reguł specyficzne dla bazy danych czasu rzeczywistego: