Multi-Tenancy-Implementierung mit PostgreSQL: Lernen Sie durch ein einfaches Beispiel aus der Praxis
Erfahren Sie, wie Sie eine Multi-Tenant-Architektur mit PostgreSQL Row-Level Security (RLS) und Datenbankrollen anhand eines realen Beispiels implementieren, um eine sichere Datenisolation zwischen den Mietern zu gewährleisten.
In einigen unserer vorherigen Artikel haben wir uns mit dem Konzept der Multi-Tenancy und seinen Anwendungen in Produkten und realen Geschäftsszenarien beschäftigt.
In diesem Artikel werden wir erkunden, wie man aus technischer Sicht eine Multi-Tenant-Architektur für Ihre Anwendung mit PostgreSQL implementiert.
Was ist Single-Tenant-Architektur?
Single-Tenant-Architektur bezieht sich auf eine Softwarearchitektur, bei der jeder Kunde eine eigene dedizierte Instanz der Anwendung und Datenbank hat.
In dieser Architektur sind die Daten und Ressourcen jedes Mieters vollständig von anderen Mietern isoliert.
Was ist Multi-Tenant-Architektur?
Multi-Tenant-Architektur ist eine Softwarearchitektur, bei der mehrere Kunden (Mieter) dieselbe Anwendungsinstanz und Infrastruktur teilen und dennoch Datenisolation beibehalten. In dieser Architektur dient eine einzige Instanz der Software mehreren Mietern, wobei die Daten jedes Mieters durch verschiedene Isolationsmechanismen von den anderen getrennt werden.
Single-Tenant-Architektur vs Multi-Tenant-Architektur
Single-Tenant-Architektur und Multi-Tenant-Architektur unterscheiden sich in Aspekten wie Datenisolation, Ressourcennutzung, Skalierbarkeit, Verwaltung und Wartung sowie Sicherheit.
In der Single-Tenant-Architektur hat jeder Kunde einen unabhängigen Datenbereich, was zu einer niedrigeren Ressourcenauslastung führt, jedoch relativ einfach für die Anpassung ist. Typischerweise ist die Single-Tenant-Software auf die spezifischen Bedürfnisse der Kunden zugeschnitten, wie z. B. Inventursysteme für einen bestimmten Stofflieferanten oder eine persönliche Blog-Webapp. Das Gemeinsame unter ihnen ist, dass jeder Kunde eine separate Instanz des Anwendungsdienstes belegt, was Anpassungen zur Erfüllung spezifischer Anforderungen erleichtert.
In einer Multi-Tenant-Architektur teilen mehrere Mieter dieselben zugrunde liegenden Ressourcen, was zu einer höheren Ressourcennutzung führt. Es ist jedoch entscheidend, Datenisolation und Sicherheit zu gewährleisten.
Multi-Tenant-Architektur ist oft die bevorzugte Softwarearchitektur, wenn Dienstanbieter standardisierte Dienste für verschiedene Kunden anbieten möchten. Diese Dienste haben normalerweise geringe Anpassungsgrade, und alle Kunden teilen dieselbe Anwendungsinstanz. Wenn eine Anwendung ein Update benötigt, ist das Aktualisieren einer Anwendungsinstanz gleichbedeutend mit dem Aktualisieren der Anwendung für alle Kunden. Beispielsweise ist CRM (Customer Relationship Management) eine standardisierte Anforderung. Diese Systeme verwenden typischerweise eine Multi-Tenant-Architektur, um denselben Dienst für alle Mieter bereitzustellen.
Strategien zur Datenisolation von Mietern in Multi-Tenant-Architektur
In einer Multi-Tenant-Architektur teilen alle Mieter dieselben zugrunde liegenden Ressourcen, weshalb die Isolierung von Ressourcen zwischen den Mietern entscheidend ist. Diese Isolierung muss nicht unbedingt physisch sein; es muss lediglich sichergestellt werden, dass Ressourcen zwischen den Mietern nicht sichtbar sind.
Im Design der Architektur können verschiedene Grade der Ressourcenisolierung zwischen Mietern erreicht werden:
Im Allgemeinen gilt: Je mehr Ressourcen zwischen den Mietern geteilt werden, desto niedriger sind die Kosten für die Systemiteration und -wartung. Umgekehrt gilt: Je weniger Ressourcen geteilt werden, desto höher sind die Kosten.
Beginn der Implementierung von Multi-Tenancy mit einem realen Beispiel
In diesem Artikel verwenden wir ein CRM-System als Beispiel, um eine einfache und dennoch praktische Multi-Tenant-Architektur vorzustellen.
Wir erkennen, dass alle Mieter dieselben Standarddienste nutzen, daher haben wir beschlossen, dass alle Mieter dieselben grundlegenden Ressourcen teilen sollen, und wir werden eine Datenisolation zwischen verschiedenen Mietern auf Datenbankebene mit der Row-Level-Security von PostgreSQL implementieren.
Zusätzlich werden wir für jeden Mieter eine separate Datenverbindung herstellen, um eine bessere Verwaltung der Mieterberechtigungen zu ermöglichen.
Als Nächstes werden wir erläutern, wie diese Multi-Tenant-Architektur implementiert wird.
Wie man Multi-Tenant-Architektur mit PostgreSQL implementiert
Fügen Sie für alle Ressourcen eine Mieterkennung hinzu
In einem CRM-System haben wir viele Ressourcen, die in verschiedenen Tabellen gespeichert sind. Beispielsweise werden Kundendaten in der Tabelle customers
gespeichert.
Vor der Implementierung von Multi-Tenancy sind diese Ressourcen nicht mit einem Mieter verknüpft:
Um die Mieter zu unterscheiden, die verschiedene Ressourcen besitzen, führen wir eine Tabelle tenants
ein, um Mieterinformationen zu speichern (wobei db_user
und db_user_password
verwendet werden, um die Datenbankverbindungsinformationen für jeden Mieter zu speichern, was weiter unten detailliert wird). Zusätzlich fügen wir jeder Ressource ein tenant_id
-Feld hinzu, um zu identifizieren, zu welchem Mieter sie gehört:
Jetzt ist jede Ressource mit einer tenant_id
verknüpft, wodurch wir theoretisch in der Lage sind, eine where
-Klausel zu allen Abfragen hinzuzufügen, um den Zugriff auf Ressourcen für jeden Mieter einzuschränken:
Auf den ersten Blick scheint dies einfach und machbar. Es wird jedoch die folgenden Probleme haben:
- Fast jede Abfrage enthält diese
where
-Klausel, was den Code überladen und schwer wartbar macht, insbesondere bei komplexen Join-Anweisungen. - Neue Mitarbeiter im Codebestand könnten leicht vergessen, diese
where
-Klausel hinzuzufügen. - Daten zwischen verschiedenen Mietern sind nicht wirklich isoliert, da jeder Mieter immer noch Berechtigungen hat, auf die Daten anderer Mieter zuzugreifen.
Daher werden wir diesen Ansatz nicht verfolgen. Stattdessen werden wir die Row-Level-Security von PostgreSQL verwenden, um diese Bedenken anzugehen. Bevor wir fortfahren, werden wir jedoch für jeden Mieter ein dediziertes Datenbankkonto erstellen, um auf diese freigegebene Datenbank zuzugreifen.
DB-Rollen für Mieter einrichten
Es ist eine gute Praxis, jedem Benutzer, der sich mit der Datenbank verbinden kann, eine Datenbankrolle zuzuordnen. Dies ermöglicht eine bessere Kontrolle über den Datenbankzugriff jedes Benutzers, erleichtert die Isolierung von Operationen zwischen verschiedenen Benutzern und verbessert die Stabilität und Sicherheit des Systems.
Da alle Mieter die gleichen Datenbankberechtigungen haben, können wir eine Basisrolle erstellen, um diese Berechtigungen zu verwalten:
Um jede Mieterrolle zu differenzieren, wird jedem Mieter bei der Erstellung eine von der Basisrolle abgeleitete Rolle zugewiesen:
Als Nächstes werden die Datenbankverbindungsinformationen für jeden Mieter in der Tabelle tenants
gespeichert:
id | db_user | db_user_password |
---|---|---|
x2euic | crm_tenant_x2euic | pa55w0rd |
Dieser Mechanismus bietet jedem Mieter seine eigene Datenbankrolle, und diese Rollen teilen die Berechtigungen, die der Rolle crm_tenant
gewährt wurden.
Wir können dann den Berechtigungsumfang für Mieter mithilfe der Rolle crm_tenant
definieren:
- Mieter sollten Lese- und Schreibzugriff auf alle Ressourcen-Tabellen des CRM-Systems haben.
- Tabellen, die nicht mit den Ressourcen des CRM-Systems in Zusammenhang stehen, sollten für die Mieter unsichtbar sein (angenommen, nur die Tabelle
systems
). - Mieter sollten die Tabelle
tenants
nicht ändern können, und nur die Felderid
unddb_user
sollten ihnen angezeigt werden, damit sie ihre eigene Mieter-ID abfragen können, wenn sie Datenbankoperationen durchführen.
Sobald die Rollen für Mieter eingerichtet sind, können wir bei einer Anfrage eines Mieters auf den Dienst mit der Datenbankrolle, die diesen Mieter repräsentiert, mit der Datenbank interagieren:
Sicherung der Mieterdaten mit PostgreSQL Row-Level Security
Bisher haben wir entsprechende Datenbankrollen für Mieter eingerichtet, aber dies beschränkt nicht den Datenzugriff zwischen den Mietern. Als Nächstes werden wir die Row-Level-Security-Funktion von PostgreSQL nutzen, um den Zugriff jedes Mieters nur auf seine eigenen Daten zu beschränken.
In PostgreSQL können Tabellen Zeilensicherheitspolicen haben, die steuern, welche Reihen von Abfragen abgerufen oder durch Datenmanipulationsbefehle geändert werden können. Diese Funktion ist auch bekannt als RLS (Row-Level Security).
Standardmäßig haben Tabellen keine Zeilensicherheitspolicen. Um RLS zu nutzen, müssen Sie es für die Tabelle aktivieren und Sicherheitspolicen erstellen, die jedes Mal ausgeführt werden, wenn auf die Tabelle zugegriffen wird.
Am Beispiel der Tabelle customers
im CRM-System werden wir RLS aktivieren und eine Sicherheitspolice erstellen, um den Zugriff jedes Mieters nur auf seine eigenen Kundendaten zu beschränken:
In der Anweisung zur Erstellung der Sicherheitspolice:
for all
(optional) gibt an, dass diese Zugangspolice fürselect
-,insert
-,update
- unddelete
-Operationen auf die Tabelle verwendet wird. Sie können eine Zugangspolice für bestimmte Operationen spezifizieren, indem Siefor
gefolgt vom Befehlskeyword verwenden.to crm_tenant
gibt an, dass diese Police für Benutzer mit der Datenbankrollecrm_tenant
gilt, also alle Mieter.as restrictive
legt den Durchsetzungsmodus der Police fest und gibt an, dass der Zugang streng limitiert sein sollte. Standardmäßig kann eine Tabelle mehrere Policen haben, mehrerepermissive
Policen werden mit einerOR
Beziehung kombiniert. In diesem Szenario deklarieren wir diese Police alsrestrictive
, weil wir möchten, dass diese Police-Prüfung obligatorisch für Benutzer ist, die zu CRM-Systemmietern gehören.using
Ausdruck definiert die Bedingungen für den tatsächlichen Zugang und beschränkt den aktuellen abfragenden Datenbankbenutzer darauf, nur Daten zu sehen, die zu seinem jeweiligen Mieter gehören. Diese Einschränkung gilt für Zeilen, die von einem Befehl (select
,update
oderdelete
) ausgewählt werden.with check
Ausdruck definiert die notwendige Einschränkung beim Ändern von Datenzeilen (insert
oderupdate
) und garantiert, dass Mieter nur Einträge für sich selbst hinzufügen oder aktualisieren können.
Die Verwendung von RLS zur Einschränkung des Mietzugriffs auf unsere Ressourcentabellen bietet mehrere Vorteile:
- Diese Police fügt effektiv
where tenant_id = (select id from tenants where db_user = current_user)
zu allen Abfrageoperationen hinzu (select
,update
oderdelete
). Wenn Sie beispielsweiseselect * from customers
ausführen, wird dies äquivalent zuselect * from customers where tenant_id = (select id from tenants where db_user = current_user)
. Dies eliminiert die Notwendigkeit,where
-Bedingungen im Anwendungscode explizit hinzuzufügen, vereinfacht ihn und reduziert die Wahrscheinlichkeit von Fehlern. - Sie kontrolliert zentral die Datenzugriffsberechtigungen zwischen verschiedenen Mietern auf Datenbankebene und mindert das Risiko von Schwachstellen oder Inkonsistenzen in der Anwendung, wodurch die Systemsicherheit verbessert wird.
Es gibt jedoch einige Punkte zu beachten:
- RLS-Policen werden für jede Datenzeile ausgeführt. Wenn die Abfragebedingungen innerhalb der RLS-Police zu komplex sind, könnte dies die Systemleistung erheblich beeinträchtigen. Glücklicherweise ist unsere Abfrage zur Überprüfung der Mandantendaten einfach genug und wird die Leistung nicht beeinträchtigen. Wenn Sie planen, später andere Funktionen mit RLS zu implementieren, können Sie den Empfehlungen zur Performance von Row-Level Security von Supabase folgen, um die RLS-Performance zu optimieren.
- RLS-Policen füllen
tenant_id
nicht automatisch währendinsert
-Operationen auf. Sie beschränken Mieter nur auf das Einfügen ihrer eigenen Daten. Das bedeutet, dass wir beim Einfügen von Daten immer noch die Mieter-ID angeben müssen, was inkonsistent mit dem Abfrageprozess ist und während der Entwicklung zu Verwirrung führen und die Wahrscheinlichkeit von Fehlern erhöhen kann (dies wird in den nächsten Schritten behandelt).
Zusätzlich zur Tabelle customers
müssen wir die gleichen Operationen auf alle Ressourcen-Tabellen des CRM-Systems anwenden (dieser Prozess kann etwas mühsam sein, aber wir können ein Programm schreiben, um dies bei der Initialisierung der Tabellen zu konfigurieren), um so die Daten der verschiedenen Mieter zu isolieren.
Trigger-Funktion für das Einfügen von Daten erstellen
Wie bereits erwähnt, ermöglicht es uns RLS (Row-Level-Security), Abfragen auszuführen, ohne uns um die Existenz von tenant_id
sorgen zu müssen, da die Datenbank dies automatisch handhabt. Für insert
-Operationen müssen wir jedoch immer noch manuell die entsprechende tenant_id
angeben.
Um ein ähnliches Maß an Bequemlichkeit wie RLS für das Einfügen von Daten zu erreichen, muss die Datenbank in der Lage sein, tenant_id
beim Einfügen von Daten automatisch zu handhaben.
Dies hat einen klaren Vorteil: Auf der Ebene der Anwendungsentwicklung müssen wir uns keine Gedanken mehr darüber machen, zu welchem Mieter die Daten gehören, was die Wahrscheinlichkeit von Fehlern verringert und die mentale Belastung beim Entwickeln von Multi-Tenant-Anwendungen verringert.
Glücklicherweise bietet PostgreSQL leistungsstarke Trigger-Funktionalitäten.
Trigger sind spezielle Funktionen, die mit Tabellen verknüpft sind und automatisch bestimmte Aktionen (wie Einfügen, Aktualisieren oder Löschen) ausführen, wenn sie auf die Tabelle angewendet werden. Diese Aktionen können auf Zeilenebene (für jede Zeile) oder Anweisungsebene (für die gesamte Anweisung) ausgelöst werden. Mit Triggern können wir benutzerdefinierte Logik vor oder nach bestimmten Datenbankoperationen ausführen, wodurch wir unser Ziel leicht erreichen können.
Erstellen wir zunächst eine Trigger-Funktion set_tenant_id
, die vor jedem Einfügen von Daten ausgeführt wird:
Als Nächstes verknüpfen wir diese Trigger-Funktion mit der Tabelle customers
für Insert-Operationen (ähnlich wie das Aktivieren von RLS für eine Tabelle, muss diese Trigger-Funktion mit allen relevanten Tabellen verknüpft werden):
Dieser Trigger stellt sicher, dass eingefügte Daten das korrekte tenant_id
enthalten. Wenn die neuen Daten bereits ein tenant_id
enthalten, tut die Trigger-Funktion nichts. Andernfalls füllt sie das tenant_id
-Feld automatisch basierend auf den Informationen des aktuellen Benutzers in der Tabelle tenants
aus.
So erreichen wir die automatische Handhabung von tenant_id
auf Datenbankebene während des Einfügens von Daten durch Mieter.
Zusammenfassung
In diesem Artikel befassen wir uns mit der praktischen Anwendung von Multi-Tenant-Architekturen und verwenden ein CRM-System als Beispiel, um eine praktische Lösung mithilfe der PostgreSQL-Datenbank aufzuzeigen.
Wir diskutieren die Verwaltung von Datenbankrollen, den Zugriffskontrollmechanismen und die Row-Level-Security-Funktion von PostgreSQL, um die Datenisolation zwischen Mietern zu gewährleisten. Außerdem nutzen wir Trigger-Funktionen, um die kognitive Belastung der Entwickler bei der Verwaltung verschiedener Mieter zu reduzieren.
Das war's für diesen Artikel. Wenn Sie Ihre Multi-Tenant-Anwendung mit dem Benutzerzugangsmanagement weiter verbessern möchten, können Sie Eine einfache Anleitung, um mit Logto-Organisationen zu beginnen - für den Aufbau einer Multi-Tenant-App für weitere Einblicke konsultieren.