Aplikasi multi-penyewa dengan alat database elastis dan keamanan tingkat baris

Berlaku untuk:Azure SQL Database

Alat database elastis dan keamanan tingkat baris (RLS) bekerja sama untuk mengaktifkan penskalaan tingkat data aplikasi multi-penyewa dengan Azure SQL Database. Bersama-sama teknologi ini membantu Anda membangun aplikasi yang memiliki tingkat data yang sangat dapat diskalakan. Tingkat data mendukung shard multi-penyewa, dan menggunakan ADO.NET SqlClient atau Kerangka Kerja Entitas. Untuk informasi selengkapnya, lihat Pola Desain untuk Aplikasi SaaS Multi-penyewa dengan Azure SQL Database.

  • Alat database elastis memungkinkan pengembang untuk meluaskan skala tingkat data dengan praktik sharding standar, dengan menggunakan pustaka .NET dan templat layanan Azure. Mengelola shard dengan menggunakan Pustaka Klien Database Elastis membantu mengotomatiskan dan mempermudah banyak tugas infrastruktur yang biasanya terkait dengan sharding.
  • Keamanan tingkat baris memungkinkan pengembang menyimpan data dengan aman untuk beberapa penyewa dalam database yang sama. Kebijakan keamanan RLS memfilter baris yang bukan milik penyewa yang menjalankan kueri. Memusatkan logika filter di dalam database menyederhanakan pemeliharaan dan mengurangi risiko kesalahan keamanan. Alternatif mengandalkan semua kode klien untuk menerapkan keamanan berisiko.

Dengan menggunakan fitur ini bersama-sama, aplikasi dapat menyimpan data untuk beberapa penyewa dalam database shard yang sama. Biayanya lebih murah per penyewa saat penyewa berbagi database. Namun aplikasi yang sama juga dapat menawarkan penyewa premiumnya pilihan untuk membayar shard penyewa tunggal khusus mereka sendiri. Salah satu keuntungan isolasi penyewa tunggal adalah jaminan performa yang lebih tegas. Dalam database penyewa tunggal, tidak ada penyewa lain yang bersaing untuk mendapatkan sumber daya.

Tujuannya adalah untuk menggunakan API perutean yang bergantung pada data pustaka klien database elastis untuk secara otomatis menyambungkan setiap penyewa tertentu ke database shard yang benar. Hanya satu shard yang berisi nilai TenantId tertentu untuk penyewa tertentu. TenantId adalah kunci sharding. Setelah koneksi dibuat, kebijakan keamanan RLS dalam database memastikan bahwa penyewa tertentu hanya dapat mengakses baris data yang berisi TenantId-nya.

Catatan

Pengidentifikasi penyewa mungkin terdiri dari lebih dari satu kolom. Untuk kenyamanan diskusi ini, kami secara informal mengasumsikan sebuah TenantId kolom tunggal.

Blogging app architecture

Mengunduh proyek sampel

Prasyarat

Proyek ini memperluas yang dideskripsikan dalam Alat DB Elastis untuk Azure SQL - Integrasi Kerangka Kerja Entitas dengan menambahkan dukungan untuk database shard multi-penyewa. Proyek ini membangun aplikasi konsol sederhana untuk membuat blog dan pos. Proyek ini mencakup empat penyewa, ditambah dua database shard multi-penyewa. Konfigurasi ini diilustrasikan dalam diagram berikut.

Membuat dan menjalankan aplikasi. Hal ini menjalankan bootstrap manajer peta shard alat database elastis, dan melakukan tes berikut:

  1. Menggunakan Kerangka Kerja Entitas dan LINQ, membuat blog baru lalu menampilkan semua blog untuk setiap penyewa
  2. Menggunakan ADO.NET SqlClient, menampilkan semua blog untuk penyewa
  3. Cobalah untuk menyisipkan blog untuk penyewa yang salah untuk memverifikasi bahwa kesalahan dilemparkan

Perhatikan bahwa karena RLS belum diaktifkan dalam database shard, masing-masing pengujian ini mengungkapkan masalah: penyewa dapat melihat blog yang bukan milik mereka, dan aplikasi tidak dicegah untuk memasukkan blog untuk penyewa yang salah. Sisa artikel ini mendeskripsikan cara menyelesaikan masalah tersebut dengan menerapkan isolasi penyewa dengan RLS. Ada dua langkah:

  1. Tingkat aplikasi: Ubah kode aplikasi untuk selalu mengatur TenantId saat ini dalam SESSION_CONTEXT setelah membuka koneksi. Contoh proyek sudah mengatur TenantId dengan cara ini.
  2. Tingkat data: Buat kebijakan keamanan RLS di setiap database shard untuk memfilter baris berdasarkan TenantId yang disimpan dalam SESSION_CONTEXT. Buat kebijakan untuk setiap database shard Anda, jika tidak, baris dalam shard multi-penyewa tidak difilter.

1. Tingkat aplikasi: Atur TenantId dalam SESSION_CONTEXT

Pertama, Anda menyambungkan ke database shard dengan menggunakan API perutean yang bergantung pada data dari pustaka klien database elastis. Aplikasi masih harus memberi tahu database TenantId mana yang menggunakan koneksi. TenantId memberi tahu kebijakan keamanan RLS baris mana yang harus difilter sebagai milik penyewa lain. Simpan TenantId saat ini dalam SESSION_CONTEXT koneksi.

Alternatif untuk SESSION_CONTEXT adalah menggunakan CONTEXT_INFO. Tetapi SESSION_CONTEXT adalah pilihan yang lebih baik. SESSION_CONTEXT lebih mudah digunakan, ini mengembalikan NULL secara default, dan mendukung pasangan kunci-nilai.

Entity Framework

Untuk aplikasi yang menggunakan Kerangka Kerja Entitas, pendekatan termudah adalah mengatur SESSION_CONTEXT dalam mengambil alih ElasticScaleContext yang dijelaskan dalam perutean yang bergantung pada data menggunakan EF DbContext. Buat dan jalankan SqlCommand yang mengatur TenantId dalam SESSION_CONTEXT ke shardingKey yang ditentukan untuk koneksi. Kemudian kembalikan koneksi yang ditengahi melalui perutean yang bergantung pada data. Dengan cara ini, Anda hanya perlu menulis kode sekali untuk 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;
    }
}
// ...

Sekarang SESSION_CONTEXT otomatis diatur dengan TenantId tertentu setiap kali ElastisScaleContext diminta:

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

Untuk aplikasi yang menggunakan ADO.NET SqlClient, buat fungsi pembungkus di sekitar metode ShardMap.OpenConnectionForKey. Buat agar pembungkus secara otomatis mengatur TenantId di SESSION_CONTEXT ke TenantId saat ini sebelum mengembalikan koneksi. Untuk memastikan bahwa SESSION_CONTEXT selalu diatur, Anda hanya boleh membuka koneksi menggunakan fungsi wrapper ini.

// 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. Tingkat data: Membuat kebijakan keamanan tingkat baris

Membuat kebijakan keamanan untuk memfilter baris yang bisa diakses setiap penyewa

Sekarang setelah aplikasi mengatur SESSION_CONTEXT dengan TenantId saat ini sebelum membuat kueri, kebijakan keamanan RLS dapat memfilter kueri dan mengecualikan baris yang memiliki TenantId berbeda.

RLS diterapkan dalam T-SQL. Fungsi yang ditentukan pengguna menentukan logika akses, dan kebijakan keamanan mengikat fungsi ini ke sejumlah tabel. Untuk proyek ini:

  1. Fungsi memverifikasi bahwa aplikasi tersambung ke database, dan bahwa TenantId yang disimpan dalam SESSION_CONTEXT cocok dengan TenantId dari baris tertentu.

    • Aplikasi telah terhubung, daripada beberapa pengguna SQL lainnya.
  2. Predikat FILTER memungkinkan baris yang memenuhi filter TenantId untuk diteruskan untuk kueri PILIH, PERBARUI, dan HAPUS.

    • Predikat BLOK mencegah baris yang menggagalkan filter menjadi diSISIPKAN atau diPERBARUI.
    • Jika SESSION_CONTEXT belum diatur, fungsi akan mengembalikan NULL, dan tidak ada baris yang terlihat atau dapat disisipkan.

Untuk mengaktifkan RLS pada semua shard, jalankan T-SQL berikut dengan menggunakan Visual Studio (SQL Server Data Tools), SQL Server Management Studio, atau skrip PowerShell yang disertakan dalam proyek. Atau jika Anda menggunakan Pekerjaan Database Elastis, Anda dapat mengotomatiskan eksekusi T-SQL ini pada semua shard.

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

Dalam proyek kompleks Anda mungkin perlu menambahkan predikat pada ratusan tabel, yang bisa membosankan. Ada prosedur yang disimpan pembantu yang secara otomatis menghasilkan kebijakan keamanan, dan menambahkan predikat pada semua tabel dalam skema. Untuk informasi selengkapnya, lihat posting blog di Menerapkan Keamanan Tingkat Baris ke semua tabel - skrip pembantu (blog).

Sekarang jika Anda menjalankan aplikasi sampel lagi, penyewa hanya melihat baris milik mereka. Selain itu, aplikasi tidak dapat menyisipkan baris milik penyewa selain yang saat ini tersambung ke database shard. Selain itu, aplikasi tidak dapat memperbarui TenantId di baris mana pun yang dapat dilihatnya. Jika aplikasi mencoba melakukannya, DbUpdateException dinaikkan.

Jika Anda menambahkan tabel baru nanti, UBAH kebijakan keamanan untuk menambahkan predikat FILTER dan BLOK pada tabel baru.

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

Menambahkan batasan default untuk mengisi TenantId secara otomatis untuk SISIPKAN

Anda dapat menempatkan batasan default pada setiap tabel untuk mengisi TenantId secara otomatis dengan nilai yang saat ini disimpan di tabel SESSION_CONTEXT saat menyisipkan baris. Contohnya adalah sebagai berikut.

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

Sekarang aplikasi tidak perlu menentukan TenantId saat menyisipkan baris:

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

Catatan

Jika Anda menggunakan batasan default untuk proyek Kerangka Kerja Entitas, disarankan agar Anda TIDAK menyertakan kolom TenantId dalam model data EF Anda. Hal ini direkomendasikan karena kueri Kerangka Kerja Entitas secara otomatis memasok nilai default yang mengambil alih batasan default yang dibuat di T-SQL yang menggunakan SESSION_CONTEXT. Untuk menggunakan batasan default dalam proyek sampel, misalnya, Anda harus menghapus TenantId dari DataClasses.cs (dan menjalankan Add-Migration di Konsol Manajer Paket) dan menggunakan T-SQL untuk memastikan bahwa bidang hanya ada dalam tabel database. Dengan cara ini, EF secara otomatis menyediakan nilai default yang salah saat menyisipkan data.

(Opsional) Mengaktifkan superuser untuk mengakses semua baris

Beberapa aplikasi mungkin ingin membuat superuser yang dapat mengakses semua baris. Superuser dapat mengaktifkan pelaporan di semua penyewa pada semua shard. Atau superuser dapat melakukan operasi split-merge pada shard yang melibatkan pemindahan baris penyewa antar database.

Untuk mengaktifkan superuser, buat pengguna SQL baru (superuser dalam contoh ini) di setiap database shard. Kemudian ubah kebijakan keamanan dengan fungsi predikat baru yang memungkinkan pengguna tersebut mengakses semua baris. Fungsi seperti itu diberikan berikutnya.

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

Pemeliharaan

  • Menambahkan shard baru: Jalankan skrip T-SQL untuk mengaktifkan RLS pada shard baru, jika tidak, kueri pada shard tersebut tidak difilter.
  • Menambahkan tabel baru: Tambahkan predikat FILTER dan BLOK ke kebijakan keamanan pada semua shard setiap kali tabel baru dibuat. Jika tidak, kueri pada tabel baru tidak difilter. Penambahan ini dapat diotomatiskan dengan menggunakan pemicu DDL, seperti yang dideskripsikan dalam Menerapkan Keamanan Tingkat Baris secara otomatis ke tabel yang baru dibuat (blog).

Ringkasan

Alat database elastis dan keamanan tingkat baris dapat digunakan bersama-sama untuk meningkatkan tingkat data aplikasi dengan dukungan untuk shard multi-penyewa dan penyewa tunggal. shard multi-penyewa dapat digunakan untuk menyimpan data secara lebih efisien. Efisiensi ini diucapkan di mana sejumlah besar penyewa hanya memiliki beberapa baris data. shard penyewa tunggal dapat mendukung penyewa premium yang memiliki persyaratan performa dan isolasi yang lebih ketat. Untuk informasi selengkapnya, lihat Referensi Keamanan Tingkat Baris.

Sumber daya tambahan

Pertanyaan dan Permintaan Fitur

Untuk pertanyaan, hubungi kami di halaman pertanyaan Microsoft Q&A untuk SQL Database. Dan tambahkan permintaan fitur apa pun ke forum umpan balik Microsoft Azure SQL Database.