Elastik veritabanı araçları ve satır düzeyi güvenlik ile çok kiracılı uygulamalar

Şunlar için geçerlidir:Azure SQL Veritabanı

Elastik veritabanı araçları ve satır düzeyi güvenlik (RLS), çok kiracılı bir uygulamanın veri katmanını Azure SQL Veritabanı ile ölçeklendirmeyi etkinleştirmek için işbirliği sağlar. Bu teknolojiler birlikte yüksek oranda ölçeklenebilir veri katmanına sahip bir uygulama oluşturmanıza yardımcı olur. Veri katmanı çok kiracılı parçaları destekler ve SqlClient veya Entity Framework ADO.NET kullanır. Daha fazla bilgi için bkz. Azure SQL Veritabanı ile Çok Kiracılı SaaS Uygulamaları için Tasarım Desenleri.

  • Elastik veritabanı araçları , geliştiricilerin .NET kitaplıklarını ve Azure hizmet şablonlarını kullanarak standart parçalama uygulamalarıyla veri katmanının ölçeğini genişletmesini sağlar. Elastik Veritabanı İstemci Kitaplığı'nı kullanarak parçaların yönetilmesi, genellikle parçalamayla ilişkili çoğu altyapı görevini otomatikleştirmeye ve kolaylaştırmaya yardımcı olur.
  • Satır düzeyi güvenlik , geliştiricilerin aynı veritabanında birden çok kiracı için verileri güvenli bir şekilde depolamasını sağlar. RLS güvenlik ilkeleri, sorguyu yürüten kiracıya ait olmayan satırları filtreler. Veritabanının içindeki filtre mantığını merkezileştirmek bakımı basitleştirir ve güvenlik hatası riskini azaltır. Güvenliği zorlamak için tüm istemci koduna güvenmenin alternatifi risklidir.

Bir uygulama, bu özellikleri birlikte kullanarak birden çok kiracının verilerini aynı parça veritabanında depolayabilir. Kiracılar bir veritabanını paylaştığında kiracı başına maliyeti daha düşük olur. Ancak aynı uygulama premium kiracılarına kendi ayrılmış tek kiracılı parçaları için ödeme seçeneği de sunabilir. Tek kiracılı yalıtımın avantajlarından biri, daha kesin performans garantileridir. Tek kiracılı bir veritabanında, kaynaklar için rekabet eden başka bir kiracı yoktur.

Amaç, her bir kiracıyı otomatik olarak doğru parça veritabanına bağlamak için elastik veritabanı istemci kitaplığı veriye bağımlı yönlendirme API'lerini kullanmaktır. Yalnızca bir parça, belirtilen kiracı için belirli bir TenantId değeri içeriyor. TenantId parçalama anahtarıdır. Bağlantı kurulduktan sonra, veritabanındaki bir RLS güvenlik ilkesi, belirtilen kiracının yalnızca TenantId değerini içeren veri satırlarına erişebilmesini sağlar.

Dekont

Kiracı tanımlayıcısı birden fazla sütundan oluşabilir. Bu tartışmanın kolaylık sağlaması için, tek sütunlu bir TenantId olduğunu resmi olmayan bir şekilde varsayıyoruz.

Blogging app architecture

Örnek projeyi indirme

Önkoşullar

Bu proje, çok kiracılı parça veritabanları için destek ekleyerek Azure SQL için Elastik DB Araçları - Entity Framework Tümleştirmesi'nde açıklananı genişletir. Proje, bloglar ve gönderiler oluşturmak için basit bir konsol uygulaması oluşturur. Proje dört kiracı ve iki çok kiracılı parça veritabanı içerir. Bu yapılandırma önceki diyagramda gösterilmiştir.

Uygulamayı derleyin ve çalıştırın. Bu çalıştırma, elastik veritabanı araçlarının parça eşleme yöneticisini bootstrap eder ve aşağıdaki testleri gerçekleştirir:

  1. Entity Framework ve LINQ kullanarak yeni bir blog oluşturun ve her kiracı için tüm blogları görüntüleyin
  2. ADO.NET SqlClient kullanarak kiracının tüm bloglarını görüntüleyin
  3. Hatanın oluştuğundan emin olmak için yanlış kiracı için bir blog eklemeyi deneyin

Parça veritabanlarında RLS henüz etkinleştirilmediğinden, bu testlerden her birinin bir sorun ortaya çıkardığını göreceksiniz: kiracılar kendilerine ait olmayan blogları görebilir ve uygulamanın yanlış kiracı için blog eklemesi engellenmez. Bu makalenin geri kalanında, RLS ile kiracı yalıtımını zorunlu kılarak bu sorunların nasıl çözüleceğini açıklanmaktadır. İki adım vardır:

  1. Uygulama katmanı: Bağlantı açıldıktan sonra SESSION_CONTEXT her zaman geçerli TenantId değerini ayarlamak için uygulama kodunu değiştirin. Örnek proje TenantId değerini zaten bu şekilde ayarlar.
  2. Veri katmanı: SESSION_CONTEXT depolanan TenantId temelinde satırları filtrelemek için her parça veritabanında bir RLS güvenlik ilkesi oluşturun. Parça veritabanlarınızın her biri için bir ilke oluşturun; aksi takdirde çok kiracılı parçalardaki satırlar filtrelenmez.

1. Uygulama katmanı: SESSION_CONTEXT TenantId değerini ayarlama

İlk olarak elastik veritabanı istemci kitaplığının verilere bağımlı yönlendirme API'lerini kullanarak bir parça veritabanına bağlanırsınız. Uygulamanın yine de veritabanına bağlantıyı hangi TenantId'nin kullandığını söylemesi gerekir. TenantId, RLS güvenlik ilkesine hangi satırların diğer kiracılara ait olarak filtrelenmeleri gerektiğini söyler. Geçerli TenantId değerini bağlantının SESSION_CONTEXT depolayın.

SESSION_CONTEXT alternatifi, CONTEXT_INFO kullanmaktır. Ancak SESSION_CONTEXT daha iyi bir seçenektir. SESSION_CONTEXT kullanımı daha kolaydır, varsayılan olarak NULL döndürür ve anahtar-değer çiftlerini destekler.

Entity Framework

Entity Framework kullanan uygulamalar için en kolay yaklaşım, EF DbContext kullanarak verilere bağımlı yönlendirme bölümünde açıklanan ElasticScaleContext geçersiz kılma içinde SESSION_CONTEXT ayarlamaktır. SESSION_CONTEXT TenantId değerini bağlantı için belirtilen parçalama Anahtarına ayarlayan bir SqlCommand oluşturun ve yürütebilirsiniz. Ardından verilere bağımlı yönlendirme aracılığıyla aracılı bağlantıyı döndürebilirsiniz. Bu şekilde, SESSION_CONTEXT ayarlamak için yalnızca bir kez kod yazmanız gerekir.

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

Artık ElasticScaleContext çağrıldığında SESSION_CONTEXT belirtilen TenantId değeriyle otomatik olarak ayarlanır:

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

SqlClient'ADO.NET

ADO.NET SqlClient kullanan uygulamalar için ShardMap.Open Bağlan ionForKey yönteminin çevresinde bir sarmalayıcı işlevi oluşturun. Sarmalayıcının bağlantı döndürmeden önce SESSION_CONTEXT tenantid değerini otomatik olarak geçerli TenantId olarak ayarlamasını sağlayın. SESSION_CONTEXT her zaman ayarlandığından emin olmak için yalnızca bu sarmalayıcı işlevini kullanarak bağlantıları açmanız gerekir.

// 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. Veri katmanı: Satır düzeyi güvenlik ilkesi oluşturma

Her kiracının erişebileceği satırları filtrelemek için bir güvenlik ilkesi oluşturma

Uygulama sorgulamadan önce geçerli TenantId ile SESSION_CONTEXT ayarlandığına göre, RLS güvenlik ilkesi sorguları filtreleyebilir ve farklı bir TenantId'ye sahip satırları dışlayabilir.

RLS, Transact-SQL'de uygulanır. Kullanıcı tanımlı bir işlev erişim mantığını tanımlar ve bir güvenlik ilkesi bu işlevi herhangi bir sayıda tabloya bağlar. Bu proje için:

  1. işlevi, uygulamanın veritabanına bağlı olduğunu ve SESSION_CONTEXT depolanan TenantId değerinin belirli bir satırın TenantId değeriyle eşleşip eşleşmediğini doğrular.

    • Uygulama, başka bir SQL kullanıcısı yerine bağlı.
  2. FILTER koşulu, TenantId filtresine uyan satırların SELECT, UPDATE ve DELETE sorguları için geçmesine olanak tanır.

    • BLOCK koşulu, filtrede başarısız olan satırların INSERTed veya UPDATEd olmasını engeller.
    • SESSION_CONTEXT ayarlanmamışsa işlev NULL döndürür ve hiçbir satır görünür olmaz veya eklenemez.

RLS'yi tüm parçalarda etkinleştirmek için Visual Studio (SSDT), SSMS veya projeye dahil edilen PowerShell betiğini kullanarak aşağıdaki T-SQL'i yürütün. Elastik Veritabanı İşleri kullanıyorsanız, tüm parçalarda bu T-SQL'in yürütülmesini otomatikleştirebilirsiniz.

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

Bahşiş

Karmaşık bir projede, koşulu yüzlerce tabloya eklemeniz gerekebilir ve bu yorucu olabilir. Otomatik olarak bir güvenlik ilkesi oluşturan ve şemadaki tüm tablolara koşul ekleyen bir yardımcı saklı yordam vardır. Daha fazla bilgi için, Tüm tablolara Satır Düzeyi Güvenlik Uygulama - yardımcı betik (blog) blog gönderisine bakın.

Şimdi örnek uygulamayı yeniden çalıştırırsanız kiracılar yalnızca onlara ait satırları görür. Buna ek olarak, uygulama şu anda parça veritabanına bağlı olandan başka kiracılara ait satırları ekleyemez. Ayrıca uygulama, kiracı kimliğini görebileceği hiçbir satırda güncelleştiremez. Uygulama bunlardan birini yapmayı denerse bir DbUpdateException oluşturulur.

Daha sonra yeni bir tablo eklerseniz, yeni tabloya FİlTRE ve BLOCK koşullarını eklemek için güvenlik ilkesini DEĞIŞTIRIN.

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

INSERT'ler için TenantId'yi otomatik olarak doldurmak için varsayılan kısıtlamalar ekleme

TenantId değerini satır eklerken SESSION_CONTEXT'de depolanan değerle otomatik olarak doldurmak için her tabloya varsayılan bir kısıtlama koyabilirsiniz. Bir örnek aşağıda verilmiştir.

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

Artık uygulamanın satır eklerken TenantId belirtmesi gerekmez:

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

Dekont

Bir Entity Framework projesi için varsayılan kısıtlamaları kullanırsanız, EF veri modelinize TenantId sütununu EKLEMEMESİnİZ önerilir. Bu önerinin nedeni, Entity Framework sorgularının T-SQL'de oluşturulan ve SESSION_CONTEXT kullanan varsayılan kısıtlamaları geçersiz kılan varsayılan değerleri otomatik olarak sağlamasıdır. Örneğin, örnek projede varsayılan kısıtlamaları kullanmak için, TenantId değerini DataClasses.cs dosyasından kaldırmanız (ve Paket Yöneticisi Konsolunda Add-Migration'ı çalıştırmanız) ve alanın yalnızca veritabanı tablolarında bulunduğundan emin olmak için T-SQL'i kullanmanız gerekir. Bu şekilde EF, veri eklerken otomatik olarak yanlış varsayılan değerler sağlar.

(İsteğe bağlı) Bir süperkullanıcının tüm satırlara erişmesini sağlama

Bazı uygulamalar tüm satırlara erişebilen bir süper kullanıcı oluşturmak isteyebilir. Süper kullanıcı, tüm parçalardaki tüm kiracılarda raporlamayı etkinleştirebilir. Veya bir süper kullanıcı, kiracı satırlarını veritabanları arasında taşımayı içeren parçalar üzerinde bölünmüş birleştirme işlemleri gerçekleştirebilir.

Bir süper kullanıcıyı etkinleştirmek için her parça veritabanında yeni bir SQL kullanıcısı (superuser bu örnekte) oluşturun. Ardından bu kullanıcının tüm satırlara erişmesini sağlayan yeni bir koşul işleviyle güvenlik ilkesini değiştirin. Bundan sonra böyle bir işlev verilir.

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

Bakım

  • Yeni parçalar ekleme: Yeni parçalarda RLS'yi etkinleştirmek için T-SQL betiğini yürütür, aksi takdirde bu parçalardaki sorgular filtrelenmez.
  • Yeni tablolar ekleme: Her yeni tablo oluşturulduğunda tüm parçalarda güvenlik ilkesine FİlTRE ve BLOCK koşulu ekleyin. Aksi takdirde yeni tablodaki sorgular filtrelenmez. Bu ekleme işlemi, Yeni oluşturulan tablolara Satır Düzeyi Güvenliği otomatik olarak uygulama (blog) bölümünde açıklandığı gibi bir DDL tetikleyicisi kullanılarak otomatikleştirilebilir.

Özet

Elastik veritabanı araçları ve satır düzeyi güvenlik, hem çok kiracılı hem de tek kiracılı parçalar için destekle bir uygulamanın veri katmanının ölçeğini genişletmek için birlikte kullanılabilir. Çok kiracılı parçalar, verileri daha verimli bir şekilde depolamak için kullanılabilir. Bu verimlilik, çok sayıda kiracının yalnızca birkaç veri satırına sahip olduğu durumlarda belirgindir. Tek kiracılı parçalar, daha katı performans ve yalıtım gereksinimlerine sahip premium kiracıları destekleyebilir. Daha fazla bilgi için bkz . Satır Düzeyi Güvenlik başvurusu.

Ek kaynaklar

Sorular ve Özellik İstekleri

Sorularınız için SQL Veritabanı için Microsoft Soru-Cevap soru sayfasından bizimle iletişime geçin. Ayrıca SQL Veritabanı geri bildirim forumunuza tüm özellik isteklerini ekleyin.