Víceklientové aplikace s nástroji elastické databáze a zabezpečením na úrovni řádků

Platí pro:Azure SQL Database

Nástroje elastické databáze a zabezpečení na úrovni řádků (RLS) spolupracují, aby bylo možné škálovat datovou vrstvu aplikace s více tenanty pomocí Azure SQL Database. Tyto technologie společně pomáhají vytvořit aplikaci, která má vysoce škálovatelnou datovou vrstvu. Datová vrstva podporuje horizontální oddíly s více tenanty a používá ADO.NET SqlClient nebo Entity Framework. Další informace najdete v tématu Vzory návrhu pro víceklientské aplikace SaaS s Azure SQL Database.

  • Nástroje elastické databáze umožňují vývojářům škálovat datovou vrstvu pomocí standardních postupů horizontálního dělení pomocí knihoven .NET a šablon služeb Azure. Správa horizontálních oddílů pomocí klientské knihovny elastické databáze pomáhá automatizovat a zjednodušit mnoho úloh infrastruktury, které jsou obvykle spojené s horizontálním dělením.
  • Zabezpečení na úrovni řádků umožňuje vývojářům bezpečně ukládat data pro více tenantů ve stejné databázi. Zásady zabezpečení na úrovni řádků vyfiltrují řádky, které nepatří do tenanta provádějícího dotaz. Centralizace logiky filtru uvnitř databáze zjednodušuje údržbu a snižuje riziko chyby zabezpečení. Alternativou spoléhat se na veškerý klientský kód k vynucování zabezpečení je rizikové.

Pomocí těchto funkcí může aplikace ukládat data pro více tenantů ve stejné databázi horizontálních oddílů. Když tenanti sdílejí databázi, stojí méně na tenanta. Stejná aplikace ale může také nabídnout svým prémiovým tenantům možnost platit za vlastní vyhrazené horizontální oddíly s jedním tenantem. Jednou z výhod izolace s jedním tenantem je pevnější záruky výkonu. V databázi s jedním tenantem neexistuje žádný jiný tenant, který by soupeřil o prostředky.

Cílem je použít rozhraní API pro směrování závislé na datové knihovně elastické databáze k automatickému připojení každého daného tenanta ke správné databázi horizontálních oddílů. Pro daného tenanta obsahuje pouze jeden horizontální oddíl konkrétní hodnotu TenantId. Id tenanta je klíč horizontálního dělení. Po navázání připojení zásady zabezpečení zabezpečení na úrovni řádků v databázi zajistí, že daný tenant bude mít přístup jenom k řádkům dat, které obsahují jeho ID tenanta.

Poznámka:

Identifikátor tenanta se může skládat z více než jednoho sloupce. Pro usnadnění této diskuze neformálně předpokládáme ID tenanta s jedním sloupcem.

Blogging app architecture

Stažení ukázkového projektu

Požadavky

Tento projekt rozšiřuje verzi popsanou v nástrojích Elastic DB pro Azure SQL – Integrace entity Framework přidáním podpory pro víceklientské databáze horizontálních oddílů. Projekt vytvoří jednoduchou konzolovou aplikaci pro vytváření blogů a příspěvků. Projekt zahrnuje čtyři tenanty a dvě databáze horizontálních oddílů s více tenanty. Tato konfigurace je znázorněna v předchozím diagramu.

Sestavte a spusťte aplikaci. Spuštěním příkazu bootstraps správce map horizontálních oddílů nástrojů elastické databáze provedete následující testy:

  1. Pomocí Entity Frameworku a LINQ vytvořte nový blog a zobrazte všechny blogy pro každého tenanta.
  2. Pomocí ADO.NET SqlClient zobrazte všechny blogy pro tenanta.
  3. Pokuste se vložit blog pro nesprávného tenanta a ověřte, že došlo k chybě.

Všimněte si, že vzhledem k tomu, že zabezpečení na úrovni řádků ještě není v databázích horizontálních oddílů povolené, každý z těchto testů odhalí problém: tenanti uvidí blogy, které do nich nepatří, a aplikace nezabrání v vložení blogu pro nesprávného tenanta. Zbývající část tohoto článku popisuje, jak tyto problémy vyřešit vynucením izolace tenanta pomocí zabezpečení na úrovni řádků. Existují dva kroky:

  1. Aplikační vrstva: Upravte kód aplikace tak, aby po otevření připojení vždy nastavil aktuální ID tenanta v SESSION_CONTEXT. Ukázkový projekt už tímto způsobem nastaví ID tenanta.
  2. Datová vrstva: V každé databázi horizontálních oddílů vytvořte zásadu zabezpečení RLS, která vyfiltruje řádky na základě ID tenanta uloženého v SESSION_CONTEXT. Vytvořte zásadu pro každou databázi horizontálních oddílů, jinak se řádky v horizontálních oddílech s více tenanty nefiltrují.

1. Aplikační vrstva: Nastavení ID tenanta v SESSION_CONTEXT

Nejprve se připojíte k databázi horizontálních oddílů pomocí rozhraní API pro směrování závislé na datech klientské knihovny elastické databáze. Aplikace stále musí informovat databázi, která TenantId používá připojení. Id tenanta říká zásadám zabezpečení zabezpečení na úrovni řádků, které řádky musí být filtrovány jako patřící jiným tenantům. Uložte aktuální ID tenanta do SESSION_CONTEXT připojení.

Alternativou k SESSION_CONTEXT je použití CONTEXT_INFO. Ale SESSION_CONTEXT je lepší volbou. SESSION_CONTEXT se snadněji používá, ve výchozím nastavení vrací hodnotu NULL a podporuje páry klíč-hodnota.

Entity Framework

Pro aplikace využívající Entity Framework je nejjednodušším přístupem nastavit SESSION_CONTEXT v rámci přepsání ElasticScaleContext popsané ve směrování závislém na datech pomocí EF DbContext. Vytvořte a spusťte SqlCommand, který nastaví Id tenanta v SESSION_CONTEXT na klíč horizontálního dělení zadaného pro připojení. Pak vrátí zprostředkované připojení prostřednictvím směrování závislého na datech. Tímto způsobem stačí napsat kód jen jednou, abyste nastavili SESSION_CONTEXT.

// 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;
    }
}
// ...

Teď se SESSION_CONTEXT automaticky nastaví se zadaným ID tenanta při každém vyvolání ElasticScaleContext:

// 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

Pro aplikace používající ADO.NET SqlClient vytvořte funkci obálky kolem metody ShardMap.Open Připojení ionForKey. Před vrácením připojení nastavte obálku automaticky v SESSION_CONTEXT na aktuální ID tenanta. Abyste měli jistotu, že je SESSION_CONTEXT vždy nastavená, měli byste otevřít pouze připojení pomocí této obálkové funkce.

// 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. Datová vrstva: Vytvoření zásad zabezpečení na úrovni řádků

Vytvoření zásady zabezpečení pro filtrování řádků, ke které má každý tenant přístup

Když teď aplikace nastavuje SESSION_CONTEXT s aktuálním ID tenanta před dotazováním, zásady zabezpečení zabezpečení na úrovni řádků můžou filtrovat dotazy a vyloučit řádky, které mají jiné ID tenanta.

Zabezpečení na úrovni řádků je implementováno v jazyce Transact-SQL. Uživatelem definovaná funkce definuje logiku přístupu a zásady zabezpečení tuto funkci sváže s libovolným počtem tabulek. Pro tento projekt:

  1. Funkce ověří, že je aplikace připojená k databázi a že ID tenanta uložené v SESSION_CONTEXT odpovídá ID tenanta daného řádku.

    • Aplikace je připojená místo jiného uživatele SQL.
  2. Predikát FILTER umožňuje předávat řádky, které splňují filtr Id tenanta, pro dotazy SELECT, UPDATE a DELETE.

    • Predikát BLOKU zabraňuje řádkům, které selžou filtr, aby byl filtr INSERTed nebo UPDATEd.
    • Pokud SESSION_CONTEXT nebyla nastavena, vrátí funkce hodnotu NULL a nejsou viditelné ani nelze vložit žádné řádky.

Pokud chcete povolit zabezpečení na úrovni řádků ve všech horizontálních oddílech, spusťte následující příkaz T-SQL pomocí sady Visual Studio (SSDT), SSMS nebo skriptu PowerShellu zahrnutého v projektu. Nebo pokud používáte úlohy elastické databáze, můžete automatizovat provádění tohoto T-SQL na všech horizontálních oddílech.

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

V komplexním projektu možná budete muset přidat predikát na stovky tabulek, což by mohlo být zdlouhavé. Existuje pomocná uložená procedura, která automaticky generuje zásady zabezpečení a přidá predikát do všech tabulek ve schématu. Další informace najdete v blogovém příspěvku o použití zabezpečení na úrovni řádků u všech tabulek – pomocný skript (blog).

Když teď ukázkovou aplikaci spustíte znovu, tenanti uvidí jenom řádky, které do nich patří. Aplikace navíc nemůže vkládat řádky, které patří do jiných tenantů, než které jsou aktuálně připojené k databázi horizontálních oddílů. Aplikace také nemůže aktualizovat ID tenanta v žádných řádcích, které uvidí. Pokud se aplikace pokusí provést některou z těchto akcí, vyvolá se výjimka DbUpdateException.

Pokud později přidáte novou tabulku, upravte zásady zabezpečení a přidejte do nové tabulky predikáty FILTER a BLOCK.

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

Přidání výchozích omezení pro automatické naplnění ID tenanta pro INSERT

Pro každou tabulku můžete nastavit výchozí omezení, které automaticky naplní Id tenanta hodnotou, která je aktuálně uložená v SESSION_CONTEXT při vkládání řádků. Následuje příklad.

-- 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

Aplikace teď při vkládání řádků nemusí zadávat ID tenanta:

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();
    }
});

Poznámka:

Pokud pro projekt Entity Framework používáte výchozí omezení, doporučujeme do datového modelu EF zahrnout sloupec TenantId. Toto doporučení je způsobeno tím, že dotazy Entity Framework automaticky poskytují výchozí hodnoty, které přepíší výchozí omezení vytvořená v jazyce T-SQL, která používají SESSION_CONTEXT. Pokud chcete použít výchozí omezení v ukázkovém projektu, měli byste například odebrat Id tenanta z souboru DataClasses.cs (a spustit doplněk v konzole Správce balíčků) a pomocí T-SQL zajistit, aby pole existovalo pouze v databázových tabulkách. Ef tak při vkládání dat automaticky zadává nesprávné výchozí hodnoty.

(Volitelné) Povolení přístupu superuživatele ke všem řádkům

Některé aplikace můžou chtít vytvořit superuživatele, který má přístup ke všem řádkům. Superuživatel může povolit vytváření sestav ve všech tenantech ve všech horizontálních oddílech. Nebo superuživatel může provádět operace dělení na horizontální oddíly, které zahrnují přesun řádků tenanta mezi databázemi.

Pokud chcete povolit superuživatele, vytvořte nového uživatele SQL (superuser v tomto příkladu) v každé databázi horizontálních oddílů. Potom upravte zásady zabezpečení pomocí nové predikátové funkce, která umožňuje tomuto uživateli přístup ke všem řádkům. Tato funkce je uvedena dále.

-- 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

Údržba

  • Přidání nových horizontálních oddílů: Spuštěním skriptu T-SQL povolte zabezpečení na úrovni řádků u všech nových horizontálních oddílů, jinak se dotazy na tyto horizontální oddíly nefiltrují.
  • Přidání nových tabulek: Přidání predikátu FILTER a BLOCK do zásad zabezpečení u všech horizontálních oddílů při každém vytvoření nové tabulky V opačném případě se dotazy na novou tabulku nefiltrují. Tento doplněk je možné automatizovat pomocí triggeru DDL, jak je popsáno v tématu Automatické použití zabezpečení na úrovni řádků u nově vytvořených tabulek (blog).

Shrnutí

Nástroje elastické databáze a zabezpečení na úrovni řádků je možné použít společně k horizontálnímu navýšení kapacity datové vrstvy aplikace s podporou více tenantů i horizontálních oddílů s jedním tenantem. Horizontální oddíly s více tenanty je možné použít k efektivnějšímu ukládání dat. Tato efektivita je výrazná, když velký počet tenantů má pouze několik řádků dat. Horizontální oddíly s jedním tenantem můžou podporovat prémiové tenanty, kteří mají přísnější požadavky na výkon a izolaci. Další informace naleznete v tématu Referenční informace k zabezpečení na úrovni řádků.

Další materiály

Dotazy a žádosti o funkce

Pokud máte dotazy, kontaktujte nás na stránce otázek Microsoft Q&A pro SLUŽBU SQL Database. A do fóra pro zpětnou vazbu ke službě SQL Database přidejte všechny žádosti o funkce.