Verwenden Sie Bedingungen in Sicherheitsregeln für Echtzeitdatenbanken

Diese Anleitung baut auf der Anleitung zum Erlernen der Kernsprache der Firebase-Sicherheitsregeln auf und zeigt, wie Sie Bedingungen zu Ihren Firebase-Echtzeitdatenbank-Sicherheitsregeln hinzufügen.

Der Hauptbaustein der Echtzeitdatenbank-Sicherheitsregeln ist die Bedingung . Eine Bedingung ist ein boolescher Ausdruck, der bestimmt, ob ein bestimmter Vorgang zugelassen oder verweigert werden soll. Bei Grundregeln funktioniert die Verwendung true und false Literale als Bedingungen hervorragend. Aber die Sprache der Echtzeitdatenbank-Sicherheitsregeln bietet Ihnen Möglichkeiten, komplexere Bedingungen zu schreiben, die Folgendes können:

  • Überprüfen Sie die Benutzerauthentifizierung
  • Bewerten Sie vorhandene Daten anhand neu übermittelter Daten
  • Greifen Sie auf verschiedene Teile Ihrer Datenbank zu und vergleichen Sie sie
  • Eingehende Daten validieren
  • Nutzen Sie die Struktur eingehender Abfragen für die Sicherheitslogik

Verwenden von $-Variablen zum Erfassen von Pfadsegmenten

Sie können Teile des Pfads für einen Lese- oder Schreibvorgang erfassen, indem Sie Erfassungsvariablen mit dem Präfix $ deklarieren. Dies dient als Platzhalter und speichert den Wert dieses Schlüssels zur Verwendung innerhalb von Regelbedingungen:

{
  "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')"
        }
      }
    }
  }
}

Die dynamischen $ -Variablen können auch parallel zu konstanten Pfadnamen verwendet werden. In diesem Beispiel verwenden wir die Variable $other , um eine .validate Regel zu deklarieren, die sicherstellt, dass widget außer title und color keine untergeordneten Elemente hat. Jeder Schreibvorgang, der zur Erstellung zusätzlicher untergeordneter Elemente führen würde, würde fehlschlagen.

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

Authentifizierung

Eines der häufigsten Sicherheitsregelmuster ist die Zugriffskontrolle basierend auf dem Authentifizierungsstatus des Benutzers. Beispielsweise möchte Ihre App möglicherweise nur angemeldeten Benutzern das Schreiben von Daten erlauben.

Wenn Ihre App die Firebase-Authentifizierung verwendet, enthält die Variable request.auth die Authentifizierungsinformationen für den Client, der Daten anfordert. Weitere Informationen zu request.auth finden Sie in der Referenzdokumentation .

Die Firebase-Authentifizierung lässt sich in die Firebase-Echtzeitdatenbank integrieren, sodass Sie den Datenzugriff auf Benutzerbasis mithilfe von Bedingungen steuern können. Sobald sich ein Benutzer authentifiziert, wird die auth in Ihren Realtime Database Security Rules-Regeln mit den Informationen des Benutzers gefüllt. Zu diesen Informationen gehören ihre eindeutige Kennung ( uid ) sowie verknüpfte Kontodaten wie eine Facebook-ID oder eine E-Mail-Adresse und andere Informationen. Wenn Sie einen benutzerdefinierten Authentifizierungsanbieter implementieren, können Sie Ihre eigenen Felder zur Authentifizierungsnutzlast Ihres Benutzers hinzufügen.

In diesem Abschnitt wird erläutert, wie Sie die Sprache der Firebase Realtime Database Security Rules mit Authentifizierungsinformationen über Ihre Benutzer kombinieren. Durch die Kombination dieser beiden Konzepte können Sie den Zugriff auf Daten basierend auf der Benutzeridentität steuern.

Die auth

Die vordefinierte auth in den Regeln ist null, bevor die Authentifizierung stattfindet.

Sobald ein Benutzer mit der Firebase-Authentifizierung authentifiziert wurde, enthält er die folgenden Attribute:

Anbieter Die verwendete Authentifizierungsmethode („Passwort“, „anonym“, „Facebook“, „Github“, „Google“ oder „Twitter“).
uid Eine eindeutige Benutzer-ID, die garantiert bei allen Anbietern eindeutig ist.
Zeichen Der Inhalt des Firebase-Auth-ID-Tokens. Weitere Einzelheiten finden Sie in der Referenzdokumentation für auth.token .

Hier ist eine Beispielregel, die die auth Variable verwendet, um sicherzustellen, dass jeder Benutzer nur in einen benutzerspezifischen Pfad schreiben kann:

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

Strukturieren Sie Ihre Datenbank zur Unterstützung von Authentifizierungsbedingungen

Normalerweise ist es hilfreich, Ihre Datenbank so zu strukturieren, dass das Schreiben von Regeln einfacher wird. Ein gängiges Muster zum Speichern von Benutzerdaten in der Echtzeitdatenbank besteht darin, alle Ihre Benutzer in einem einzelnen users zu speichern, dessen untergeordnete Elemente die uid Werte für jeden Benutzer sind. Wenn Sie den Zugriff auf diese Daten so einschränken möchten, dass nur der angemeldete Benutzer seine eigenen Daten sehen kann, würden Ihre Regeln in etwa so aussehen.

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

Arbeiten mit benutzerdefinierten Authentifizierungsansprüchen

Für Apps, die eine benutzerdefinierte Zugriffskontrolle für verschiedene Benutzer erfordern, ermöglicht die Firebase-Authentifizierung Entwicklern , Ansprüche für einen Firebase-Benutzer festzulegen . Auf diese Ansprüche kann über die Variable auth.token in Ihren Regeln zugegriffen werden. Hier ist ein Beispiel für Regeln, die den benutzerdefinierten Anspruch hasEmergencyTowel verwenden:

{
  "rules": {
    "frood": {
      // A towel is about the most massively useful thing an interstellar
      // hitchhiker can have
      ".read": "auth.token.hasEmergencyTowel === true"
    }
  }
}

Entwickler, die ihre eigenen benutzerdefinierten Authentifizierungstoken erstellen, können diesen Token optional Ansprüche hinzufügen. Diese Ansprüche sind für die Variable auth.token in Ihren Regeln verfügbar.

Vorhandene Daten vs. neue Daten

Die vordefinierte data wird verwendet, um auf die Daten zu verweisen, bevor ein Schreibvorgang stattfindet. Umgekehrt enthält die Variable newData die neuen Daten, die vorhanden sind, wenn der Schreibvorgang erfolgreich ist. newData stellt das zusammengeführte Ergebnis der neu geschriebenen Daten und der vorhandenen Daten dar.

Zur Veranschaulichung: Diese Regel würde es uns ermöglichen, neue Datensätze zu erstellen oder vorhandene zu löschen, aber keine Änderungen an vorhandenen Nicht-Null-Daten vorzunehmen:

// 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()"

Referenzieren von Daten in anderen Pfaden

Als Kriterium für Regeln können beliebige Daten verwendet werden. Mithilfe der vordefinierten Variablen root , data und newData können wir auf jeden Pfad zugreifen, wie er vor oder nach einem Schreibereignis vorhanden wäre.

Betrachten Sie dieses Beispiel, das Schreibvorgänge zulässt, solange der Wert des Knotens /allow_writes/ true ist, für den übergeordneten Knoten kein readOnly Flag gesetzt ist und in den neu geschriebenen Daten ein untergeordneter Knoten namens foo vorhanden ist:

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

Daten validieren

Die Durchsetzung von Datenstrukturen und die Validierung des Formats und Inhalts von Daten sollten mithilfe von .validate Regeln erfolgen, die erst ausgeführt werden, nachdem eine .write Regel erfolgreich Zugriff gewährt hat. Nachfolgend finden Sie ein Beispiel für eine .validate -Regeldefinition, die nur Datumsangaben im Format JJJJ-MM-TT zwischen den Jahren 1900 und 2099 zulässt, was mithilfe eines regulären Ausdrucks überprüft wird.

".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])$/)"

Die .validate Regeln sind der einzige Sicherheitsregeltyp, der nicht kaskadiert. Wenn eine Validierungsregel für einen untergeordneten Datensatz fehlschlägt, wird der gesamte Schreibvorgang abgelehnt. Darüber hinaus werden die Validierungsdefinitionen ignoriert, wenn Daten gelöscht werden (d. h. wenn der neue Wert, der geschrieben wird, null ist).

Dies mag wie triviale Punkte erscheinen, tatsächlich handelt es sich jedoch um wichtige Funktionen zum Schreiben leistungsstarker Firebase-Echtzeitdatenbank-Sicherheitsregeln. Beachten Sie die folgenden Regeln:

{
  "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()"
      }
    }
  }
}

Sehen Sie sich unter Berücksichtigung dieser Variante die Ergebnisse für die folgenden Schreibvorgänge an:

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);
Ziel c
Hinweis: Dieses Firebase-Produkt ist auf dem App Clip-Ziel nicht verfügbar.
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];
Schnell
Hinweis: Dieses Firebase-Produkt ist auf dem App Clip-Ziel nicht verfügbar.
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);
AUSRUHEN
# 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

Schauen wir uns nun die gleiche Struktur an, verwenden jedoch .write Regeln anstelle von .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()"
      }
    }
  }
}

In dieser Variante wäre jede der folgenden Operationen erfolgreich:

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);
Ziel c
Hinweis: Dieses Firebase-Produkt ist auf dem App Clip-Ziel nicht verfügbar.
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];
Schnell
Hinweis: Dieses Firebase-Produkt ist auf dem App Clip-Ziel nicht verfügbar.
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);
AUSRUHEN
# 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

Dies veranschaulicht die Unterschiede zwischen .write und .validate Regeln. Wie gezeigt, sollten alle diese Regeln mit .validate geschrieben werden, mit der möglichen Ausnahme der newData.hasChildren() Regel, die davon abhängt, ob Löschungen zulässig sein sollen.

Abfragebasierte Regeln

Obwohl Sie Regeln nicht als Filter verwenden können , können Sie den Zugriff auf Teilmengen von Daten beschränken, indem Sie Abfrageparameter in Ihren Regeln verwenden. query. Ausdrücke in Ihren Regeln, um Lese- oder Schreibzugriff basierend auf Abfrageparametern zu gewähren.

Die folgende abfragebasierte Regel verwendet beispielsweise benutzerbasierte Sicherheitsregeln und abfragebasierte Regeln, um den Zugriff auf Daten in der baskets nur auf die Einkaufskörbe zu beschränken, deren Eigentümer der aktive Benutzer ist:

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

Die folgende Abfrage, die die Abfrageparameter in der Regel enthält, wäre erfolgreich:

db.ref("baskets").orderByChild("owner")
                 .equalTo(auth.currentUser.uid)
                 .on("value", cb)                 // Would succeed

Allerdings würden Abfragen, die die Parameter in der Regel nicht enthalten, mit einem PermissionDenied Fehler fehlschlagen:

db.ref("baskets").on("value", cb)                 // Would fail with PermissionDenied

Sie können auch abfragebasierte Regeln verwenden, um zu begrenzen, wie viele Daten ein Client durch Lesevorgänge herunterlädt.

Die folgende Regel beschränkt beispielsweise den Lesezugriff nur auf die ersten 1000 Ergebnisse einer Abfrage, geordnet nach Priorität:

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)

Die folgende query. Ausdrücke sind in Echtzeit-Datenbanksicherheitsregeln verfügbar.

Abfragebasierte Regelausdrücke
Ausdruck Typ Beschreibung
query.orderByKey
query.orderByPriority
query.orderByValue
Boolescher Wert True für Abfragen, die nach Schlüssel, Priorität oder Wert sortiert sind. Sonst falsch.
query.orderByChild Zeichenfolge
Null
Verwenden Sie eine Zeichenfolge, um den relativen Pfad zu einem untergeordneten Knoten darzustellen. Beispiel: query.orderByChild === "address/zip" . Wenn die Abfrage nicht nach einem untergeordneten Knoten sortiert ist, ist dieser Wert null.
query.startAt
query.endAt
query.equalTo
Zeichenfolge
Nummer
Boolescher Wert
Null
Ruft die Grenzen der ausführenden Abfrage ab oder gibt null zurück, wenn kein gebundener Satz vorhanden ist.
query.limitToFirst
query.limitToLast
Nummer
Null
Ruft den Grenzwert für die ausgeführte Abfrage ab oder gibt null zurück, wenn kein Grenzwert festgelegt ist.

Nächste Schritte

Nach dieser Erörterung der Bedingungen verfügen Sie über ein umfassenderes Verständnis der Regeln und sind bereit für Folgendes:

Erfahren Sie, wie Sie mit Kernanwendungsfällen umgehen und lernen Sie den Workflow zum Entwickeln, Testen und Bereitstellen von Regeln kennen:

Lernen Sie Regelfunktionen kennen, die speziell für die Echtzeitdatenbank gelten: