Toepassingen met meerdere tenants met hulpprogramma's voor elastische databases en beveiliging op rijniveau

Van toepassing op: Azure SQL Database

Hulpprogramma's voor elastische databases en beveiliging op rijniveau (RLS) werken samen om het schalen van de gegevenslaag van een toepassing met meerdere tenants met Azure SQL Database mogelijk te maken. Samen helpen deze technologieën u bij het bouwen van een toepassing met een zeer schaalbare gegevenslaag. De gegevenslaag ondersteunt shards met meerdere tenants en maakt gebruik van ADO.NET SqlClient of Entity Framework. Zie Ontwerppatronen voor SaaS-toepassingen met meerdere tenants met Azure SQL Database voor meer informatie.

  • Met hulpprogramma's voor elastische databases kunnen ontwikkelaars de gegevenslaag uitschalen met standaard shardingprocedures met behulp van .NET-bibliotheken en Azure-servicesjablonen. Het beheren van shards met behulp van de elastic database-clientbibliotheek helpt bij het automatiseren en stroomlijnen van veel van de infrastructuurtaken die doorgaans zijn gekoppeld aan sharding.
  • Met beveiliging op rijniveau kunnen ontwikkelaars veilig gegevens opslaan voor meerdere tenants in dezelfde database. Beveiligingsbeleidsregels voor beveiliging op rijniveau filteren rijen die geen deel uitmaken van de tenant die een query uitvoert. Het centraliseren van de filterlogica in de database vereenvoudigt het onderhoud en vermindert het risico op een beveiligingsfout. Het alternatief voor het afdwingen van alle clientcode is riskant.

Door deze functies samen te gebruiken, kan een toepassing gegevens opslaan voor meerdere tenants in dezelfde sharddatabase. Het kost minder per tenant wanneer de tenants een database delen. Maar dezelfde toepassing kan ook premium tenants de mogelijkheid bieden om te betalen voor hun eigen toegewezen shard met één tenant. Eén voordeel van isolatie met één tenant is betere prestatiegaranties. In een database met één tenant concurreren er geen andere tenant voor resources.

Het doel is om de gegevensafhankelijke routerings-API's van de clientbibliotheek voor elastische databases te gebruiken om elke opgegeven tenant automatisch te verbinden met de juiste sharddatabase. Slechts één shard bevat een bepaalde TenantId-waarde voor de opgegeven tenant. De TenantId is de sharding-sleutel. Nadat de verbinding tot stand is gebracht, zorgt een RLS-beveiligingsbeleid binnen de database ervoor dat de opgegeven tenant alleen toegang heeft tot de gegevensrijen die de tenant-id bevatten.

Notitie

De tenant-id kan uit meer dan één kolom bestaan. Voor het gemak gaan we informeel uit van een TenantId met één kolom.

Blogging app architecture

Het voorbeeldproject downloaden

Vereisten

Dit project breidt het project uit dat wordt beschreven in Elastic DB Tools for Azure SQL - Entity Framework Integration door ondersteuning toe te voegen voor sharddatabases met meerdere tenants. Het project bouwt een eenvoudige consoletoepassing voor het maken van blogs en berichten. Het project bevat vier tenants, plus twee sharddatabases met meerdere tenants. Deze configuratie wordt geïllustreerd in het voorgaande diagram.

Maak de toepassing en voer deze uit. Met deze uitvoering wordt de shard-toewijzingsbeheer voor elastic database-hulpprogramma's opgestart en worden de volgende tests uitgevoerd:

  1. Maak met Entity Framework en LINQ een nieuw blog en geef vervolgens alle blogs voor elke tenant weer
  2. Met ADO.NET SqlClient geeft u alle blogs voor een tenant weer
  3. Probeer een blog in te voegen voor de verkeerde tenant om te controleren of er een fout is opgetreden

U ziet dat omdat RLS nog niet is ingeschakeld in de sharddatabases, elk van deze tests een probleem vertoont: tenants kunnen blogs zien die er niet bij horen en de toepassing wordt niet verhinderd om een blog in te voegen voor de verkeerde tenant. In de rest van dit artikel wordt beschreven hoe u deze problemen kunt oplossen door tenantisolatie af te dwingen met RLS. Er zijn twee stappen:

  1. Toepassingslaag: wijzig de toepassingscode om altijd de huidige TenantId in de SESSION_CONTEXT in te stellen nadat u een verbinding hebt geopend. In het voorbeeldproject wordt de TenantId al op deze manier ingesteld.
  2. Gegevenslaag: maak een RLS-beveiligingsbeleid in elke sharddatabase om rijen te filteren op basis van de TenantId die is opgeslagen in SESSION_CONTEXT. Maak een beleid voor elk van uw sharddatabases, anders worden rijen in shards met meerdere tenants niet gefilterd.

1. Toepassingslaag: TenantId instellen in de SESSION_CONTEXT

Eerst maakt u verbinding met een sharddatabase met behulp van de gegevensafhankelijke routerings-API's van de clientbibliotheek voor elastische databases. De toepassing moet nog steeds de database vertellen welke TenantId de verbinding gebruikt. De TenantId vertelt het RLS-beveiligingsbeleid welke rijen moeten worden gefilterd als behorend tot andere tenants. Sla de huidige TenantId op in de SESSION_CONTEXT van de verbinding.

Een alternatief voor SESSION_CONTEXT is het gebruik van CONTEXT_INFO. Maar SESSION_CONTEXT is een betere optie. SESSION_CONTEXT gemakkelijker te gebruiken is, retourneert deze standaard NULL en ondersteunt sleutel-waardeparen.

Entity Framework

Voor toepassingen die Entity Framework gebruiken, is de eenvoudigste methode om de SESSION_CONTEXT in te stellen binnen de ElasticScaleContext-onderdrukking die wordt beschreven in gegevensafhankelijke routering met EF DbContext. Maak en voer een SqlCommand uit waarmee TenantId in de SESSION_CONTEXT wordt ingesteld op de shardingKey die is opgegeven voor de verbinding. Retourneer vervolgens de verbinding die is brokered via gegevensafhankelijke routering. Op deze manier hoeft u maar één keer code te schrijven om de SESSION_CONTEXT in te stellen.

// ElasticScaleContext.cs
// Constructor for data-dependent routing.
// This call opens a validated connection that is routed to the
// proper shard by the shard map manager.
// Note that the base class constructor call fails for an open connection
// if migrations need to be done and SQL credentials are used.
// This is the reason for the separation of constructors.
// ...
public ElasticScaleContext(ShardMap shardMap, T shardingKey, string connectionStr)
    : base(
        OpenDDRConnection(shardMap, shardingKey, connectionStr),
        true)  // contextOwnsConnection
{
}

public static SqlConnection OpenDDRConnection(
    ShardMap shardMap,
    T shardingKey,
    string connectionStr)
{
    // No initialization.
    Database.SetInitializer<ElasticScaleContext<T>>(null);

    // Ask shard map to broker a validated connection for the given key.
    SqlConnection conn = null;
    try
    {
        conn = shardMap.OpenConnectionForKey(
            shardingKey,
            connectionStr,
            ConnectionOptions.Validate);

        // Set TenantId in SESSION_CONTEXT to shardingKey
        // to enable Row-Level Security filtering.
        SqlCommand cmd = conn.CreateCommand();
        cmd.CommandText =
            @"exec sp_set_session_context
                @key=N'TenantId', @value=@shardingKey";
        cmd.Parameters.AddWithValue("@shardingKey", shardingKey);
        cmd.ExecuteNonQuery();

        return conn;
    }
    catch (Exception)
    {
        if (conn != null)
        {
            conn.Dispose();
        }
        throw;
    }
}
// ...

De SESSION_CONTEXT wordt nu automatisch ingesteld met de opgegeven TenantId wanneer ElasticScaleContext wordt aangeroepen:

// Program.cs
SqlDatabaseUtils.SqlRetryPolicy.ExecuteAction(() =>
{
    using (var db = new ElasticScaleContext<int>(
        sharding.ShardMap, tenantId, connStrBldr.ConnectionString))
    {
        var query = from b in db.Blogs
                    orderby b.Name
                    select b;

        Console.WriteLine("All blogs for TenantId {0}:", tenantId);
        foreach (var item in query)
        {
            Console.WriteLine(item.Name);
        }
    }
});

ADO.NET SqlClient

Voor toepassingen die ADO.NET SqlClient gebruiken, maakt u een wrapper-functie rond de methode ShardMap.Open Verbinding maken ionForKey. Laat de wrapper TenantId in de SESSION_CONTEXT automatisch instellen op de huidige TenantId voordat u een verbinding retourneert. Om ervoor te zorgen dat SESSION_CONTEXT altijd is ingesteld, moet u alleen verbindingen openen met deze wrapper-functie.

// Program.cs
// Wrapper function for ShardMap.OpenConnectionForKey() that
// automatically sets SESSION_CONTEXT with the correct
// tenantId before returning a connection.
// As a best practice, you should only open connections using this method
// to ensure that SESSION_CONTEXT is always set before executing a query.
// ...
public static SqlConnection OpenConnectionForTenant(
    ShardMap shardMap, int tenantId, string connectionStr)
{
    SqlConnection conn = null;
    try
    {
        // Ask shard map to broker a validated connection for the given key.
        conn = shardMap.OpenConnectionForKey(
            tenantId, connectionStr, ConnectionOptions.Validate);

        // Set TenantId in SESSION_CONTEXT to shardingKey
        // to enable Row-Level Security filtering.
        SqlCommand cmd = conn.CreateCommand();
        cmd.CommandText =
            @"exec sp_set_session_context
                @key=N'TenantId', @value=@shardingKey";
        cmd.Parameters.AddWithValue("@shardingKey", tenantId);
        cmd.ExecuteNonQuery();

        return conn;
    }
    catch (Exception)
    {
        if (conn != null)
        {
            conn.Dispose();
        }
        throw;
    }
}

// ...

// Example query via ADO.NET SqlClient.
// If row-level security is enabled, only Tenant 4's blogs are listed.
SqlDatabaseUtils.SqlRetryPolicy.ExecuteAction(() =>
{
    using (SqlConnection conn = OpenConnectionForTenant(
        sharding.ShardMap, tenantId4, connStrBldr.ConnectionString))
    {
        SqlCommand cmd = conn.CreateCommand();
        cmd.CommandText = @"SELECT * FROM Blogs";

        Console.WriteLine(@"--
All blogs for TenantId {0} (using ADO.NET SqlClient):", tenantId4);

        SqlDataReader reader = cmd.ExecuteReader();
        while (reader.Read())
        {
            Console.WriteLine("{0}", reader["Name"]);
        }
    }
});

2. Gegevenslaag: Beveiligingsbeleid op rijniveau maken

Een beveiligingsbeleid maken om de rijen te filteren die elke tenant kan openen

Nu de toepassing SESSION_CONTEXT instelt met de huidige TenantId voordat er query's worden uitgevoerd, kan een RLS-beveiligingsbeleid query's filteren en rijen uitsluiten die een andere TenantId hebben.

RLS wordt geïmplementeerd in Transact-SQL. Een door de gebruiker gedefinieerde functie definieert de toegangslogica en een beveiligingsbeleid verbindt deze functie met een willekeurig aantal tabellen. Voor dit project:

  1. De functie controleert of de toepassing is verbonden met de database en of de TenantId die is opgeslagen in de SESSION_CONTEXT overeenkomt met de TenantId van een bepaalde rij.

    • De toepassing is verbonden in plaats van een andere SQL-gebruiker.
  2. Met een filterpredicaat kunnen rijen die voldoen aan het TenantId-filter doorgegeven voor SELECT-, UPDATE- en DELETE-query's.

    • Met een BLOKpredicaat voorkomt u dat rijen die mislukken in het filter INSERTed of UPDATEd zijn.
    • Als SESSION_CONTEXT niet is ingesteld, retourneert de functie NULL en zijn er geen rijen zichtbaar of kunnen ze worden ingevoegd.

Als u RLS wilt inschakelen voor alle shards, voert u de volgende T-SQL uit met behulp van Visual Studio (SSDT), SSMS of het PowerShell-script dat in het project is opgenomen. Of als u elastische databasetaken gebruikt, kunt u de uitvoering van deze T-SQL op alle shards automatiseren.

CREATE SCHEMA rls; -- Separate schema to organize RLS objects.
GO

CREATE FUNCTION rls.fn_tenantAccessPredicate(@TenantId int)
    RETURNS TABLE
    WITH SCHEMABINDING
AS
    RETURN SELECT 1 AS fn_accessResult
        -- Use the user in your application's connection string.
        -- Here we use 'dbo' only for demo purposes!
        WHERE DATABASE_PRINCIPAL_ID() = DATABASE_PRINCIPAL_ID('dbo')
        AND CAST(SESSION_CONTEXT(N'TenantId') AS int) = @TenantId;
GO

CREATE SECURITY POLICY rls.tenantAccessPolicy
    ADD FILTER PREDICATE rls.fn_tenantAccessPredicate(TenantId) ON dbo.Blogs,
    ADD BLOCK  PREDICATE rls.fn_tenantAccessPredicate(TenantId) ON dbo.Blogs,
    ADD FILTER PREDICATE rls.fn_tenantAccessPredicate(TenantId) ON dbo.Posts,
    ADD BLOCK  PREDICATE rls.fn_tenantAccessPredicate(TenantId) ON dbo.Posts;
GO

Tip

In een complex project moet u mogelijk het predicaat toevoegen aan honderden tabellen, wat tijdrovend kan zijn. Er is een opgeslagen helperprocedure die automatisch een beveiligingsbeleid genereert en een predicaat toevoegt aan alle tabellen in een schema. Zie het blogbericht bij Beveiliging op rijniveau toepassen op alle tabellen - helperscript (blog) voor meer informatie.

Als u de voorbeeldtoepassing nu opnieuw uitvoert, zien tenants alleen rijen die bij hen horen. Bovendien kan de toepassing geen rijen invoegen die deel uitmaken van andere tenants dan de tenant die momenteel is verbonden met de sharddatabase. Bovendien kan de app de TenantId niet bijwerken in rijen die deze kan zien. Als de app een van beide probeert te doen, wordt er een DbUpdateException gegenereerd.

Als u later een nieuwe tabel toevoegt, wijzigt u het beveiligingsbeleid om FILTER- en BLOCK-predicaten toe te voegen aan de nieuwe tabel.

ALTER SECURITY POLICY rls.tenantAccessPolicy
    ADD FILTER PREDICATE rls.fn_tenantAccessPredicate(TenantId) ON dbo.MyNewTable,
    ADD BLOCK  PREDICATE rls.fn_tenantAccessPredicate(TenantId) ON dbo.MyNewTable;
GO

Standaardbeperkingen toevoegen om TenantId automatisch te vullen voor INSERTs

U kunt voor elke tabel een standaardbeperking instellen om de TenantId automatisch te vullen met de waarde die momenteel is opgeslagen in SESSION_CONTEXT bij het invoegen van rijen. Hier volgt een voorbeeld.

-- Create default constraints to auto-populate TenantId with the
-- value of SESSION_CONTEXT for inserts.
ALTER TABLE Blogs
    ADD CONSTRAINT df_TenantId_Blogs
    DEFAULT CAST(SESSION_CONTEXT(N'TenantId') AS int) FOR TenantId;
GO

ALTER TABLE Posts
    ADD CONSTRAINT df_TenantId_Posts
    DEFAULT CAST(SESSION_CONTEXT(N'TenantId') AS int) FOR TenantId;
GO

De toepassing hoeft nu geen TenantId op te geven bij het invoegen van rijen:

SqlDatabaseUtils.SqlRetryPolicy.ExecuteAction(() =>
{
    using (var db = new ElasticScaleContext<int>(
        sharding.ShardMap, tenantId, connStrBldr.ConnectionString))
    {
        // The default constraint sets TenantId automatically!
        var blog = new Blog { Name = name };
        db.Blogs.Add(blog);
        db.SaveChanges();
    }
});

Notitie

Als u standaardbeperkingen gebruikt voor een Entity Framework-project, is het raadzaam dat u de kolom TenantId niet opneemt in uw EF-gegevensmodel. Deze aanbeveling is omdat Entity Framework-query's automatisch standaardwaarden leveren die de standaardbeperkingen overschrijven die zijn gemaakt in T-SQL die gebruikmaken van SESSION_CONTEXT. Als u standaardbeperkingen in het voorbeeldproject wilt gebruiken, moet u bijvoorbeeld TenantId verwijderen uit DataClasses.cs (en invoegtoepassingsmigratie uitvoeren in de Pakketbeheer Console) en T-SQL gebruiken om ervoor te zorgen dat het veld alleen in de databasetabellen bestaat. Op deze manier levert EF automatisch onjuiste standaardwaarden op bij het invoegen van gegevens.

(Optioneel) Een supergebruiker toegang geven tot alle rijen

Sommige toepassingen willen mogelijk een superuser maken die toegang heeft tot alle rijen. Een supergebruiker kan rapportage inschakelen voor alle tenants op alle shards. Of een superuser kan bewerkingen voor splitsen samenvoegen uitvoeren op shards waarbij tenantrijen tussen databases moeten worden verplaatst.

Als u een superuser wilt inschakelen, maakt u een nieuwe SQL-gebruiker (superuser in dit voorbeeld) in elke sharddatabase. Wijzig vervolgens het beveiligingsbeleid met een nieuwe predicaatfunctie waarmee deze gebruiker toegang heeft tot alle rijen. Een dergelijke functie krijgt de volgende.

-- New predicate function that adds superuser logic.
CREATE FUNCTION rls.fn_tenantAccessPredicateWithSuperUser(@TenantId int)
    RETURNS TABLE
    WITH SCHEMABINDING
AS
    RETURN SELECT 1 AS fn_accessResult
        WHERE
        (
            DATABASE_PRINCIPAL_ID() = DATABASE_PRINCIPAL_ID('dbo') -- Replace 'dbo'.
            AND CAST(SESSION_CONTEXT(N'TenantId') AS int) = @TenantId
        )
        OR
        (
            DATABASE_PRINCIPAL_ID() = DATABASE_PRINCIPAL_ID('superuser')
        );
GO

-- Atomically swap in the new predicate function on each table.
ALTER SECURITY POLICY rls.tenantAccessPolicy
    ALTER FILTER PREDICATE rls.fn_tenantAccessPredicateWithSuperUser(TenantId) ON dbo.Blogs,
    ALTER BLOCK  PREDICATE rls.fn_tenantAccessPredicateWithSuperUser(TenantId) ON dbo.Blogs,
    ALTER FILTER PREDICATE rls.fn_tenantAccessPredicateWithSuperUser(TenantId) ON dbo.Posts,
    ALTER BLOCK  PREDICATE rls.fn_tenantAccessPredicateWithSuperUser(TenantId) ON dbo.Posts;
GO

Onderhoud

  • Nieuwe shards toevoegen: Voer het T-SQL-script uit om RLS in te schakelen op nieuwe shards, anders worden query's op deze shards niet gefilterd.
  • Nieuwe tabellen toevoegen: Voeg een FILTER- en BLOKpredicaat toe aan het beveiligingsbeleid voor alle shards wanneer er een nieuwe tabel wordt gemaakt. Anders worden query's in de nieuwe tabel niet gefilterd. Deze toevoeging kan worden geautomatiseerd met behulp van een DDL-trigger, zoals wordt beschreven in Beveiliging op rijniveau automatisch toepassen op zojuist gemaakte tabellen (blog).

Samenvatting

Hulpprogramma's voor elastische databases en beveiliging op rijniveau kunnen samen worden gebruikt om de gegevenslaag van een toepassing uit te schalen met ondersteuning voor zowel shards met meerdere tenants als voor één tenant. Shards met meerdere tenants kunnen worden gebruikt om gegevens efficiënter op te slaan. Deze efficiëntie wordt uitgesproken wanneer een groot aantal tenants slechts een paar rijen gegevens bevat. Shards met één tenant kunnen premium tenants ondersteunen die strengere prestatie- en isolatievereisten hebben. Zie De referentie voor beveiliging op rijniveau voor meer informatie.

Aanvullende bronnen

Vragen en functieaanvragen

Neem voor vragen contact met ons op op de microsoft Q&A-vragenpagina voor SQL Database. En voeg eventuele functieaanvragen toe aan het feedbackforum van SQL Database.