• saas
  • multi-tenancy
  • postgres
  • row-level security
  • rls
  • trigger function
  • multi-tenant architecture
  • single-tenant architecture

Implementacja multi-tenancy z PostgreSQL: Naucz się przez prosty przykład z rzeczywistego świata

Dowiedz się, jak wdrożyć architekturę multi-tenant z PostgreSQL Row-Level Security (RLS) i rolami bazy danych na przykładzie z rzeczywistego świata w celu izolacji danych między najemcami.

Yijun
Yijun
Developer

W niektórych z naszych poprzednich artykułów zagłębiliśmy się w koncepcję multi-tenancy i jej zastosowania w produktach oraz rzeczywistych scenariuszach biznesowych.

W tym artykule dowiemy się, jak zaimplementować architekturę multi-tenant dla twojej aplikacji, korzystając z PostgreSQL z technicznego punktu widzenia.

Co to jest architektura single-tenant?

Architektura single-tenant odnosi się do architektury oprogramowania, w której każdy klient ma swoją własną dedykowaną instancję aplikacji i bazy danych.

W tej architekturze dane i zasoby każdego najemcy są całkowicie odizolowane od innych najemców.

Single-tenancy

Co to jest architektura multi-tenant?

Architektura multi-tenant to architektura oprogramowania, w której wielu klientów (najemcy) dzieli tę samą instancję aplikacji i infrastrukturę, jednocześnie zachowując izolację danych. W tej architekturze jedna instancja oprogramowania obsługuje wielu najemców, a dane każdego z nich są oddzielone od innych za pomocą różnych mechanizmów izolacji.

Multi-tenancy

Architektura single-tenant vs multi-tenant

Architektura single-tenant i multi-tenant różnią się w aspektach takich jak izolacja danych, wykorzystanie zasobów, skalowalność, zarządzanie i utrzymanie oraz bezpieczeństwo.

W architekturze single-tenant każdy klient ma niezależną przestrzeń danych, co prowadzi do mniejszego wykorzystania zasobów, ale relatywnie prostsze dla personalizacji. Zazwyczaj oprogramowanie single-tenant jest dostosowane do specyficznych potrzeb klienta, takich jak systemy inwentaryzacji dla konkretnego dostawcy tkanin lub osobista aplikacja blogowa. Wspólną cechą jest to, że każdy klient posiada oddzielną instancję usługi aplikacji, co ułatwia dostosowanie do specyficznych wymagań.

W architekturze multi-tenant wielu najemców dzieli te same zasoby podłoża, co skutkuje wyższym wykorzystaniem zasobów. Jednak kluczowe jest zapewnienie izolacji danych i bezpieczeństwa.

Architektura multi-tenant jest często preferowaną architekturą oprogramowania, gdy dostawcy usług oferują standardowe usługi dla różnych klientów. Usługi te zazwyczaj mają niski poziom personalizacji, a wszyscy klienci korzystają z tej samej instancji aplikacji. Kiedy aplikacja wymaga aktualizacji, zaktualizowanie jednej instancji aplikacji jest równoznaczne z aktualizacją aplikacji dla wszystkich klientów. Na przykład CRM (Customer Relationship Management) jest standaryzowanym wymaganiem. Systemy te zazwyczaj używają architektury multi-tenant, aby zapewnić tę samą usługę wszystkim najemcom.

Strategie izolacji danych najemcy w architekturze multi-tenant

W architekturze multi-tenant wszyscy najemcy dzielą te same zasoby podłoża, co czyni izolację zasobów między najemcami kluczową. Izolacja ta nie musi koniecznie być fizyczna; wystarczy zapewnienie, że zasoby między najemcami nie są widoczne dla siebie nawzajem.

W projekcie architektury można osiągnąć różne stopnie izolacji zasobów między najemcami:

Isolated to shared

Generalnie, im więcej zasobów dzielonych wśród najemców, tym niższy koszt iteracji i utrzymania systemu. Odwrotnie, im mniej zasobów dzielonych, tym wyższy koszt.

Rozpoczęcie implementacji multi-tenancy na rzeczywistym przykładzie

W tym artykule użyjemy systemu CRM jako przykładu, aby przedstawić prostą, ale praktyczną architekturę multi-tenant.

Zdajemy sobie sprawę, że wszyscy najemcy korzystają z tych samych standardowych usług, więc postanowiliśmy, aby wszyscy najemcy dzielili te same podstawowe zasoby i zaimplementowaliśmy izolację danych między różnymi najemcami na poziomie bazy danych, korzystając z PostgreSQL'owego Row-Level Security.

Dodatkowo, stworzymy oddzielne połączenie z danymi dla każdego najemcy, aby ułatwić lepsze zarządzanie uprawnieniami najemców.

Następnie przedstawimy, jak zaimplementować tę architekturę multi-tenant.

Jak zaimplementować architekturę multi-tenant z PostgreSQL

Dodaj identyfikator najemcy do wszystkich zasobów

W systemie CRM będziemy mieć wiele zasobów, które są przechowywane w różnych tabelach. Na przykład, informacje o klientach są przechowywane w tabeli customers.

Przed zaimplementowaniem multi-tenancy te zasoby nie są powiązane z żadnym najemcą:

Aby odróżnić najemców posiadających różne zasoby, wprowadzamy tabelę tenants do przechowywania informacji o najemcach (gdzie db_user i db_user_password są używane do przechowywania informacji o połączeniu z bazą danych dla każdego najemcy, szczegóły poniżej). Dodatkowo dodajemy pole tenant_id do każdego zasobu, aby zidentyfikować, do którego najemcy należy:

Teraz każdy zasób jest powiązany z tenant_id, teoretycznie umożliwiając nam dodanie klauzuli where do wszystkich zapytań, aby ograniczyć dostęp do zasobów dla każdego najemcy:

Na pierwszy rzut oka wydaje się to proste i wykonalne. Jednak będzie miało to następujące problemy:

  • Prawie każde zapytanie będzie zawierać tę klauzulę where, co zaśmieca kod i utrudnia konserwację, zwłaszcza gdy piszemy złożone zapytania łączące.
  • Nowicjusze w bazie kodu mogą łatwo zapomnieć o dodaniu tej klauzuli where.
  • Dane między różnymi najemcami nie są naprawdę izolowane, ponieważ każdy najemca nadal ma uprawnienia do dostępu do danych należących do innych najemców.

Dlatego nie przyjmiemy tego podejścia. Zamiast tego użyjemy PostgreSQL'owego Row Level Security, aby rozwiązać te problemy. Jednak przed kontynuowaniem utworzymy dedykowane konto bazy danych dla każdego najemcy do dostępu do tej współdzielonej bazy danych.

Konfiguruj role DB dla najemców

Dobrą praktyką jest przypisanie roli bazy danych do każdego użytkownika, który może łączyć się z bazą danych. Pozwala to na lepszą kontrolę nad dostępem każdego użytkownika do bazy danych, ułatwiając izolację operacji między różnymi użytkownikami i poprawiając stabilność i bezpieczeństwo systemu.

Ponieważ wszyscy najemcy mają te same uprawnienia do operacji na bazie danych, możemy utworzyć bazową rolę do zarządzania tymi uprawnieniami:

Następnie, aby rozróżnić role każdego najemcy, rola odziedziczenia z bazowej roli jest przypisywana do każdego najemcy podczas tworzenia:

Następnie informacje o połączeniu z bazą danych każdego najemcy będą przechowywane w tabeli tenants:

iddb_userdb_user_password
x2euiccrm_tenant_x2euicpa55w0rd

Ten mechanizm zapewnia każdemu najemcy własną rolę bazy danych, a te role dzielą uprawnienia przypisane roli crm_tenant.

Następnie możemy zdefiniować zakres uprawnień dla najemców przy użyciu roli crm_tenant:

  • Najemcy powinni mieć dostęp CRUD do wszystkich tabel zasobów systemu CRM.
  • Tabele niezwiązane z zasobami systemu CRM powinny być niewidoczne dla najemców (zakładając tylko tabelę systems).
  • Najemcy nie powinni mieć możliwości modyfikowania tabeli tenants, a jedynie pola id i db_user powinny być widoczne dla nich do zapytań o własne id najemcy podczas wykonywania operacji na bazie danych.

Po ustawieniu ról dla najemców, gdy najemca zażąda dostępu do usługi, możemy interakcjonować z bazą danych za pomocą roli bazy danych reprezentującej tego najemcę:

Zabezpiecz dane najemcy przy użyciu PostgreSQL Row-Level Security

Do tej pory ustanowiliśmy odpowiednie role bazy danych dla najemców, ale to nie ogranicza dostępu do danych między najemcami. Następnie wykorzystamy funkcję PostgreSQL Row-Level Security, aby ograniczyć dostęp każdego najemcy do własnych danych.

W PostgreSQL tabele mogą mieć polityki bezpieczeństwa wiersza, które kontrolują, które wiersze mogą być dostępne przez zapytania lub modyfikowane przez operacje manipulacji danymi. Ta funkcja jest również znana jako RLS (Row-Level Security).

Domyślnie tabele nie mają polityk bezpieczeństwa wiersza. Aby wykorzystać RLS, musisz włączyć go dla tabeli i utworzyć polityki bezpieczeństwa, które będą wykonywać za każdym razem, gdy tabela jest dostępna.

Biorąc pod uwagę tabelę customers w systemie CRM jako przykład, włączymy RLS i utworzymy politykę bezpieczeństwa, aby ograniczyć każdego najemcę do dostępu tylko do własnych danych klientów:

W oświadczeniu tworzącym politykę bezpieczeństwa:

  • for all (opcjonalnie) oznacza, że ta polityka dostępu będzie używana dla operacji select, insert, update i delete na tabeli. Możesz określić politykę dostępu dla konkretnych operacji za pomocą for po którym następuje słowo kluczowe komendy.
  • to crm_tenant oznacza, że ta polityka dotyczy użytkowników z rolą bazy danych crm_tenant, czyli wszystkich najemców.
  • as restrictive określa tryb egzekwowania polityki, wskazując, że dostęp powinien być ściśle ograniczony. Domyślnie tabela może mieć wiele polityk, wiele polityk permissive będzie łączone z relacją OR. W tym scenariuszu deklarujemy tę politykę jako restrictive, ponieważ chcemy, aby ten sprawdzian polityki był obowiązkowy dla użytkowników należących do najemców systemu CRM.
  • using wyrażenie definiuje warunki dla faktycznego dostępu, ograniczając obecnego użytkownika bazy danych do przeglądania tylko danych należących do ich odpowiedniego najemcy. To ograniczenie dotyczy wierszy wybranych przez komendę (select, update lub delete).
  • with check wyrażenie definiuje ograniczenie wymagane przy modyfikacji wierszy danych (insert lub update), gwarantując, że najemcy mogą dodawać lub aktualizować rekordy tylko dla siebie.

Używanie RLS do ograniczenia dostępu najemców do naszych tabel zasobów oferuje kilka korzyści:

  • Ta polityka skutecznie dodaje where tenant_id = (select id from tenants where db_user = current_user) do wszystkich operacji zapytań (select, update lub delete). Na przykład, gdy wykonujesz select * from customers, jest to równoważne z wykonaniem select * from customers where tenant_id = (select id from tenants where db_user = current_user). To eliminuje potrzebę wyraźnego dodawania warunków where w kodzie aplikacji, upraszczając go i zmniejszając prawdopodobieństwo błędów.
  • Centralnie kontroluje dostęp do danych między różnymi najemcami na poziomie bazy danych, zmniejszając ryzyko luk bezpieczeństwa lub niespójności w aplikacji, zwiększając tym samym bezpieczeństwo systemu.

Jednak są pewne punkty do uwagi:

  • Polityki RLS są wykonywane dla każdego wiersza danych. Jeśli warunki zapytań w polityce RLS są zbyt złożone, może to znacznie wpłynąć na wydajność systemu. Na szczęście, nasze zapytanie sprawdzające dane najemców jest na tyle proste, że nie wpłynie na wydajność. Jeśli planujesz później zaimplementować inne funkcjonalności przy użyciu RLS, możesz skorzystać z rekomendacji wydajności RLS z Supabase w celu optymalizacji wydajności RLS.
  • Polityki RLS nie automatycznie wypełniają tenant_id podczas operacji insert. Ograniczają jedynie, aby najemcy dodawali własne dane. Oznacza to, że wstawiając dane, nadal musimy podać identyfikator najemcy, co jest niezgodne z procesem przeszukiwania i może prowadzić do zamieszania podczas rozwoju, zwiększając prawdopodobieństwo błędów (to będzie omówione w dalszych krokach).

Oprócz tabeli customers, musimy zastosować te same operacje do wszystkich tabel zasobów systemu CRM (ten proces może być nieco żmudny, ale możemy napisać program do skonfigurowania tego podczas inicjalizacji tabeli), tym samym izolując dane z różnych najemców.

Utwórz funkcję wyzwalacza dla wstawiania danych

Jak wspomniano wcześniej, RLS (Row-Level Security) pozwala nam na wykonywanie zapytań bez martwienia się o istnienie tenant_id, ponieważ baza danych obsługuje to automatycznie. Jednak dla operacji insert, nadal musimy ręcznie określić odpowiedni tenant_id.

Aby osiągnąć podobną wygodę jak RLS dla wstawiania danych, potrzebujemy, aby baza danych była w stanie automatycznie obsłużyć tenant_id podczas wstawiania danych.

To ma wyraźną korzyść: na poziomie rozwoju aplikacji nie musimy już rozważać, do którego najemcy należą dane, zmniejszając prawdopodobieństwo błędów i łagodząc nasze obciążenie umysłowe podczas rozwoju aplikacji multi-tenant.

Na szczęście PostgreSQL zapewnia potężne funkcje wyzwalania.

Wyzwalacze to specjalne funkcje powiązane z tabelami, które automatycznie wykonują określone akcje (takie jak insert, update, lub delete) podczas wykonywania na tabeli. Te akcje mogą być wyzwalane na poziomie wiersza (dla każdego wiersza) lub poziomie oświadczenia (dla całego oświadczenia). Dzięki wyzwalaczom możemy wykonać niestandardową logikę przed lub po określonych operacjach bazy danych, co pozwala nam łatwo osiągnąć nasz cel.

Najpierw, utwórzmy funkcję wyzwalacza set_tenant_id, która będzie wykonywana przed każdym wstawianiem danych:

Następnie powiąż tę funkcję wyzwalacza z tabelą customers dla operacji wstawiania (podobnie do włączania RLS dla tabeli, ta funkcja wyzwalacza musi być powiązana z wszystkimi odpowiednimi tabelami):

Ten wyzwalacz zapewnia, że wstawiane dane zawierają poprawny tenant_id. Jeśli nowe dane już zawierają tenant_id, funkcja wyzwalacza nic nie robi. W przeciwnym razie automatycznie wypełnia pole tenant_id na podstawie informacji bieżącego użytkownika w tabeli tenants.

W ten sposób osiągamy automatyczną obsługę tenant_id na poziomie bazy danych podczas wstawiania danych przez najemców.

Podsumowanie

W tym artykule zagłębiliśmy się w praktyczne zastosowanie architektury multi-tenant, używając systemu CRM jako przykładu, aby zaprezentować praktyczne rozwiązanie z wykorzystaniem bazy danych PostgreSQL.

Dyskutujemy o zarządzaniu rolami bazy danych, kontroli dostępu i funkcji Row-Level Security PostgreSQL, aby zapewnić izolację danych między najemcami. Dodatkowo, korzystamy z funkcji wyzwalaczy, aby zmniejszyć obciążenie poznawcze programistów w zarządzaniu różnymi najemcami.

To wszystko na ten artykuł. Jeśli chcesz dalej wzmacniać swoją aplikację multi-tenant za pomocą zarządzania dostępem użytkowników, możesz odnieść się do Łatwego przewodnika po rozpoczęciu z organizacjami Logto - dla budowania aplikacji multi-tenant dla bardziej wnikliwych informacji.