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

Implementering av multi-tenancy med PostgreSQL: Lär dig genom ett enkelt verklighetsexempel

Lär dig hur du implementerar multi-tenant-arkitektur med PostgreSQL Row-Level Security (RLS) och databaseroller genom ett verklighetsexempel för säker dataisolering mellan hyresgäster.

Yijun
Yijun
Developer

I några av våra tidigare artiklar har vi fördjupat oss i konceptet multi-tenancy och dess tillämpningar i produkterna och verkliga affärsscenarier.

I den här artikeln kommer vi att utforska hur du implementerar en multi-tenant-arkitektur för din applikation med hjälp av PostgreSQL ur ett tekniskt perspektiv.

Vad är single-tenant-arkitektur?

Single-tenant-arkitektur syftar på en mjukvaruarkitektur där varje kund har sin egen dedikerade instans av applikationen och databasen.

I denna arkitektur är varje hyresgästs data och resurser helt isolerade från andra hyresgäster.

Single-tenancy

Vad är multi-tenant-arkitektur?

Multi-tenant-arkitektur är en mjukvaruarkitektur där flera kunder (hyresgäster) delar samma applikationsinstans och infrastruktur samtidigt som dataisolering upprätthålls. I denna arkitektur betjänar en enda instans av mjukvaran flera hyresgäster, med varje hyresgästs data hållna åtskilda från andra genom olika isoleringsmekanismer.

Multi-tenancy

Single-tenant-arkitektur vs multi-tenant-arkitektur

Single-tenant-arkitektur och multi-tenant-arkitektur skiljer sig åt i aspekter som dataisolering, resursanvändning, skalbarhet, förvaltning och underhåll, och säkerhet.

I single-tenant-arkitektur har varje kund ett självständigt datarum, vilket leder till lägre resursanvändning men relativt enklare för anpassning. Vanligtvis är single-tenant-mjukvara skräddarsydd för specifika kundbehov, som inventeringssystem för en viss textilleverantör eller en personlig bloggsajt. Gemensamt för dem är att varje kund har en separat instans av applikationstjänsten, vilket underlättar anpassning för att möta specifika krav.

I en multi-tenant-arkitektur delar flera hyresgäster samma underliggande resurser, vilket resulterar i högre resursutnyttjande. Emellertid är det viktigt att säkerställa dataisolering och säkerhet.

Multi-tenant-arkitektur är ofta den föredragna mjukvaruarkitekturen när tjänsteleverantörer erbjuder standardiserade tjänster till olika kunder. Dessa tjänster har vanligtvis låga anpassningsnivåer, och alla kunder delar samma applikationsinstans. När en applikation behöver uppdateras, innebär uppdateringen av en applikationsinstans att applikationen uppdateras för alla kunder. CRM (Customer Relationship Management) är exempelvis ett standardbehov. Dessa system använder typiskt en multi-tenant-arkitektur för att tillhandahålla samma tjänst till alla hyresgäster.

Strategier för hyresgästdataseparation i multi-tenant-arkitektur

I en multi-tenant-arkitektur delar alla hyresgäster samma underliggande resurser, vilket gör resursisolering mellan hyresgäster avgörande. Denna isolering behöver inte nödvändigtvis vara fysisk; det kräver bara att resurser mellan hyresgäster inte är synliga för varandra.

I utformningen av arkitekturen kan olika grader av resursisolering mellan hyresgäster uppnås:

Isolerat till delat

Generellt gäller att ju fler resurser som delas mellan hyresgäster, desto lägre är kostnaden för systemets iteration och underhåll. Omvänt, desto färre resurser som delas, desto högre är kostnaden.

Starta multi-tenant-implementeringen med ett verklighetsexempel

I den här artikeln kommer vi att använda ett CRM-system som exempel för att introducera en enkel men praktisk multi-tenant-arkitektur.

Vi inser att alla hyresgäster använder samma standardtjänster, så vi har beslutat att alla hyresgäster delar samma grundläggande resurser, och vi kommer att implementera dataisolering mellan olika hyresgäster på databasenivå med PostgreSQL:s Row-Level Security.

Dessutom kommer vi att skapa en separat databasanslutning för varje hyresgäst för att underlätta bättre hantering av hyresgästrättigheter.

Nästa steg kommer vi att introducera hur man implementerar denna multi-tenant-arkitektur.

Hur man implementerar multi-tenant-arkitektur med PostgreSQL

Lägg till hyresgästidentifierare för alla resurser

I ett CRM-system kommer vi att ha många resurser och de lagras i olika tabeller. Till exempel lagras kundinformation i tabellen customers.

Innan du implementerar multi-tenancy är dessa resurser inte kopplade till någon hyresgäst:

För att särskilja de hyresgäster som äger olika resurser, introducerar vi en tenants-tabell för att lagra hyresgästinformation (där db_user och db_user_password används för att lagra databasanslutningsinformationen för varje hyresgäst, kommer att förklaras nedan). Dessutom lägger vi till ett fält tenant_id till varje resurs för att identifiera vilken hyresgäst det tillhör:

Nu är varje resurs kopplad till en tenant_id, vilket teoretiskt sett möjliggör att vi kan lägga till en where-klausul till alla frågor för att begränsa åtkomsten till resurser för varje hyresgäst:

Vid första anblicken verkar detta enkelt och genomförbart. Emellertid kommer det att ha följande problem:

  • Nästan varje fråga kommer att inkludera denna where-klausul, vilket förvirrar koden och gör den svårare att underhålla, särskilt när man skriver komplexa join-satser.
  • Nykomlingar i koden kan lätt glömma att lägga till denna where-klausul.
  • Data mellan olika hyresgäster är inte riktigt isolerade, eftersom varje hyresgäst fortfarande har rättigheter att komma åt data som tillhör andra hyresgäster.

Därför kommer vi inte att anta detta tillvägagångssätt. Istället kommer vi att använda PostgreSQL's Row-Level Security för att åtgärda dessa problem. Men innan vi fortsätter kommer vi att skapa ett dedikerat databaskonto för varje hyresgäst för att komma åt denna delade databas.

Ställ in DB-roller för hyresgäster

Det är en bra praxis att tilldela en databaseroll till varje användare som kan ansluta till databasen. Detta gör det möjligt för bättre kontroll över varje användares åtkomst till databasen och underlättar isolering av operationer mellan olika användare och förbättrar systemets stabilitet och säkerhet.

Eftersom alla hyresgäster har samma databasoperationstillstånd, kan vi skapa en basroll för att hantera dessa rättigheter:

För att särskilja varje hyresgästs roll skapas en roll som ärver från basrollen och tilldelas varje hyresgäst vid skapelsen:

Därefter lagras databasanslutningsinformationen för varje hyresgäst i tenants-tabellen:

iddb_userdb_user_password
x2euiccrm_tenant_x2euicpa55w0rd

Denna mekanism förser varje hyresgäst med sin egen databaseroll, och dessa roller delar de rättigheter som beviljats ​​till crm_tenant-rollen.

Vi kan sedan definiera behörighetsområdet för hyresgäster genom att använda crm_tenant-rollen:

  • Hyresgäster bör ha CRUD-åtkomst till alla CRM-systemresurstabeller.
  • Tabeller som inte är relaterade till CRM-systemresurser bör vara osynliga för hyresgäster (antar endast systems-tabell).
  • Hyresgäster bör inte kunna ändra tenants-tabellen, och endast id och db_user-fälten bör vara synliga för dem för att fråga om deras eget hyresgäst-id vid databasoperationer.

När rollerna för hyresgäster är inställda, när en hyresgäst begär åtkomst till tjänsten, kan vi interagera med databasen med hjälpt av den databaseroll som representerar den hyresgästen:

Säkerställ hyresgästdatan med PostgreSQL Row-Level Security

Hittills har vi etablerat motsvarande databaseroller för hyresgäster, men detta begränsar inte dataåtkomst mellan hyresgäster. Nästa steg är att utnyttja PostgreSQL's Row-Level Security för att begränsa varje hyresgäst åtkomst till deras egna data.

I PostgreSQL kan tabeller ha row security policies som kontrollerar vilka rader som kan nås av frågor eller ändras av datamanipuleringskommandon. Den här funktionen är också känd som RLS (Row-Level Security).

Som standard har tabeller inga rowsäkerhetspolicyer. För att använda RLS måste du aktivera det för tabellen och skapa säkerhetspolicyer som verkställs varje gång tabellen nås.

Med hjälp av customers-tabellen i CRM-systemet som exempel, aktiverar vi RLS och skapar en säkerhetspolicy för att begränsa varje hyresgäst att endast ha åtkomst till data från sina egna kunder:

I uttalandet som skapar säkerhetspolicyn:

  • for all (valfritt) indikerar att denna åtkomstpolicy kommer att användas för select, insert, update och delete operationer på tabellen. Du kan specificera en åtkomstpolicy för specifika operationer med for följt av kommandonyckelordet.
  • to crm_tenant indikerar att denna policy gäller användare med databaserollen crm_tenant, vilket betyder alla hyresgäster.
  • as restrictive anger vilken genomförandeläge som gäller för policyn, vilket indikerar att åtkomst bör begränsas strikt. Som standard kan en tabell ha flera policys, flera permissive policys kombineras med en OR relation. I detta scenario deklarerar vi denna policy som restrictive eftersom vi vill att denna politiska kontroll ska vara obligatorisk för användare som hör till CRM-systemhyresgäster.
  • using-uttrycket definierar villkoren för faktisk åtkomst, vilket begränsar den aktuella frågande databasanvändaren till att endast se data som tillhör deras respektive hyresgäst. Denna begränsning gäller för rader som valts ut av ett kommando (select, update eller delete).
  • with check-uttrycket definierar begränsningen som krävs vid modifiering av datarader (insert eller update), vilket garanterar att hyresgäster bara kan lägga till eller uppdatera poster för sig själva.

Att använda RLS för att begränsa hyresgästers åtkomst till våra resurser erbjuder flera fördelar:

  • Denna policy lägger i praktiken till where tenant_id = (select id from tenants where db_user = current_user) till alla frågeoperationer (select, update eller delete). Till exempel, när du kör select * from customers, är det likvärdigt med att köra select * from customers where tenant_id = (select id from tenants where db_user = current_user). Detta eliminerar behovet av att explicit lägga till where-villkor i applikationskoden, vilket förenklar den och minskar risken för fel.
  • Den kontrollerar centralt åtkomst till data mellan olika hyresgäster på databasenivå, vilket minskar risken för sårbarheter eller inkonsekvenser i applikationen och förbättrar systemets säkerhet.

Det finns dock några punkter att notera:

  • RLS-policys körs för varje datarow. Om frågevillkoren inom RLS-policys är för komplexa kan det ha betydande påverkan på systemets prestanda. Lyckligtvis är vår kontrollfråga för hyresgästdata enkel nog och kommer inte att påverka prestandan. Om du planerar att implementera andra funktioner med RLS senare, kan du följa Row-Level Security prestandarekommendationer från Supabase för att optimera RLS-prestanda.
  • RLS-policys fyller inte automatiskt i tenant_id under insert-operationer. De begränsar bara hyresgäster till att lägga in sina egna data. Detta innebär att när man inför data, måste vi fortfarande ge hyresgäst-ID själv, vilket inte är konsekvent med frågeprocessen och kan leda till förvirring under utvecklingen, vilket ökar risken för fel (detta kommer att lösas i efterföljande steg).

Förutom tabellen customers, måste vi tillämpa samma operationer på alla CRM-systemresurstabeller (denna process kan vara lite tråkig, men vi kan skriva ett program för att konfigurera det under tabellinitialiseringen), så att data från olika hyresgäster hålls åtskilda.

Skapa triggerfunktion för datainföring

Som tidigare nämnts, möjliggör RLS (Row-Level Security) att vi kan köra frågor utan att oroa oss för existensen av tenant_id, eftersom databasen hanterar det automatiskt. Dock, för insert-operationer, behöver vi fortfarande manuellt specificera motsvarande tenant_id.

För att uppnå samma bekvämlighet som RLS vid datainföring, måste databasen kunna hantera tenant_id automatiskt under datainföring.

Detta har en klar fördel: Utvecklingsnivå, vi behöver inte längre överväga vilken hyresgäst datan tillhör, vilket minskar sannolikheten för fel och underlättar vårt mentala arbete vid utveckling av multi-tenant-applikationer.

Lyckligtvis erbjuder PostgreSQL kraftfull triggerfunktionalitet.

Triggers är speciella funktioner kopplade till tabeller som automatiskt genomför specifika åtgärder (såsom infoga, uppdatera eller ta bort) när de utförs på tabellen. Dessa åtgärder kan utföras på radrivå (för varje rad) eller uttalnivå (för hela uttalandet). Med triggers kan vi köra kundanpassad logik före eller efter specifika databasoperationer, vilket låter oss enkelt nå vårt mål.

Först, låt oss skapa en triggerfunktion set_tenant_id att köras före varje datainföring:

Nästa, associera denna triggerfunktion med customers-tabellen för insert-operationer (liknande processen för att aktivera RLS för en tabell, måste denna triggerfunktion associeras med alla relevanta tabeller):

Denna trigger säkerställer att införd data innehåller korrekt tenant_id. Om den nya datan redan inkluderar en tenant_id, gör triggerfunktionen ingenting. Annars fyller den automatiskt tenant_id-fältet baserat på informationen om den aktuella användaren i tenants-tabellen.

På detta sätt uppnår vi automatisk hantering av tenant_id på databasenivå under datainföringen av hyresgäster.

Sammanfattning

I den här artikeln fördjupade vi i praktisk tillämpning av multi-tenant-arkitektur, med ett CRM-system som ett exempel för att demonstrera en praktisk lösning med PostgreSQL-databas.

Vi diskuterade hantering av databaseroller, åtkomstkontroll och PostgreSQL's Row-Level Security-funktion för att säkerställa dataisolering mellan hyresgäster. Dessutom utnyttjade vi triggerfunktioner för att minska den kognitiva bördan för utvecklare vid hantering av olika hyresgäster.

Det var allt för den här artikeln. Om du vill ytterligare förbättra din multi-tenant-applikation med användaråtkomsthantering, kan du hänvisa till En enkel guide för att börja med Logto-organisationer - för att bygga en multi-tenant-app för mer insikter.