تطبيقات متعددة المستأجرين مزودة بأدوات قاعدة بيانات مرنة وأمن على مستوى الصف

ينطبق على: قاعدة بيانات Azure SQL

تتعاون أدوات قاعدة البيانات المرنة والأمان على مستوى الصف (RLS)، لتمكين تحجيم الطبقة المسؤولة عن بيانات التطبيقات متعددة المستأجرين باستخدام قاعدة بيانات Azure SQL. سوف تساعدك كل هذه التقنيات على إنشاء تطبيق يتمتع بطبقة مسؤولة عن البيانات قابلة للتحجيم بشكل كبير. تعتمد الطبقة المسؤولة عن البيانات القطع متعددة المستأجرين، ويستخدم ADO.NET SqlClient أو Entity Framework. للحصول على مزيدٍ من المعلومات، راجع تصميم الأنماط لتطبيقات خدمة تأجير البرامج (SaaS) متعددة المستأجرين باستخدام قاعدة بيانات Azure SQL.

  • أدوات قاعدة البيانات المرنة تمكّن المطورين من توسيع الطبقة المسؤولة عن البيانات من خلال ممارسات التقسيم القياسية، وذلك باستخدام مكتبات .NET، وقوالب خدمة Azure. إن إدارة القطع باستخدام مكتبة عميل قاعدة البيانات المرنة يساعد على أتمتة العديد من مهام البنية التحتية المرتبطة عادةً مع التقسيم وتبسيطها.
  • يمكن الأمان على مستوى الصف المطورين من تخزين البيانات بأمان، لمستأجرين متعددين، في نفس قاعدة البيانات. تعمل نُهج أمان RLS على استبعاد الصفوف التي لا تنتمي إلى المستأجر الذي ينفذ الاستعلام. إن تمركز منطق عامل التصفية داخل قاعدة البيانات يُسهل عمليات الصيانة، ويحد من مخاطر حدوث أخطاء تتعلق بالأمان. إنه البديل المتمثل في الاعتماد على جميع التعليمات البرمجية الخاصة بالعميل لفرض الأمان المحفوف بالمخاطر.

باستخدام هذه الميزات معاً، يمكن تطبيق تخزين بيانات مستأجرين متعددين في قاعدة بيانات نفس القطعة. يكلف الأمر تكاليف أقل لكل مستأجر، عندما يشارك قاعدة البيانات. ولكن، إن نفس التطبيق يمكن أن يقدم للمستأجرين المتميزين خيار الدفع مقابل الحصول على القطعة المخصصة الخاصة بهم لمستأجر واحد. من مزايا عزل المستأجر الواحد، هو ضمان الأداء الأكثر صرامة. في قاعدة البيانات المخصصة لمستأجر واحد، لا يوجد مستأجر آخر يتنافس على الموارد.

والهدف من ذلك هو استخدام واجهات برمجة التطبيقات (APIs) للتوجيه المعتمد على بيانات مكتبة عميل قاعدة البيانات المرنة، للاتصال تلقائياً إلى كل مستأجر موجود، إلى قاعدة بيانات القطعة الصحيحة. تحتوي القطعة الواحدة فقط على قيمة TenantId معينة، للمستأجر المحدد. وTenantId هو مفتاح التقسيم. بعد تأسيس الاتصال، يضمن نهج أمان RLS ضمن قاعدة البيانات، أن المستأجر المحدد يمكنه الوصول إلى صفوف البيانات التي تحتوي على TenantId الخاص به فقط.

ملاحظة

قد يتكون معرف المستأجر من أكثر من عمود واحد. وتسهيلاً للنقاش، نحن نفترض بشكل غير رسمي وجود TenantId يحتوي على صف واحد.

Blogging app architecture

تنزيل نموذج المشروع

المتطلبات الأساسية

يقوم هذا المشروع بتوسيع المشروع الموصوف في أدوات قاعدة البيانات المرنة لـ Azure SQL - Entity Framework Integration، من خلال إضافة الدعم إلى قواعد بيانات القطعة متعددة المستأجرين. ينشئ المشروع تطبيق وحدة التحكم البسيط، لإنشاء المدونات والمنشورات. ويشمل المشروع أربعة مستأجرين، بالإضافة إلى قاعدتي بيانات قطعة متعددة المستأجرين. يتم توضيح هذا التكوين في المخطط السابق.

إنشاء التطبيق وتشغيله. يؤدي هذا التشغيل إلى تشغيل التمهيدي لإدارة تعيين القطعة الخاصة بأدوات قاعدة البيانات المرنة، ويجري الاختبارات التالية:

  1. مستخدماً Entity Framework، وLINQ، أنشئ مدوّنة جديدة، ثم اعرض جميع المدونات لكل مستأجر
  2. باستخدام ADO.NET SqlClient، اعرض كل المدونات للمستأجر
  3. حاول إدراج مدوّنة ما للمستأجر الخطأ، للتحقق من طرح الخطأ

لاحظ أنه نظرا لأن RLS لم يتم تمكينه بعد في قواعد بيانات القطعة، فكل من هذه الاختبارات تكشف عن مشكلة: المستأجرون قادرون على رؤية المدونات التي لا ينتمون إليها، والتطبيق لا يمنع إدراج مدونة للمستأجر الخطأ. يوضح الجزء المتبقي من هذه المقالة كيفية حل هذه المشكلات من خلال تنفيذ عزل المستأجر باستخدام RLS. هناك خطوتان:

  1. طبقة التطبيق: تعديل التعليمة البرمجية للتطبيق لتعيين TenantId الحالي دائماً في SESSION_CONTEXT بعد فتح اتصال ما. عينة المشروع بالفعل تُعيّن TenantId بهذه الطريقة.
  2. طبقة البيانات: أنشئ نهج أمان RLS في كل قاعدة بيانات جزئية لتصفية الصفوف بناءً على TenantId المخزن في SESSION_CONTEXT. أنشئ نهجاً لكل من قواعد بيانات القطع، وإلا لن يتم تصفية الصفوف في القطع متعددة المستأجرين.

1. طبقة التطبيق: تعيين TenantId في SESSION_CONTEXT

أولاً، يمكنك الاتصال بقاعدة بيانات القطعة باستخدام واجهات برمجة تطبيقات (APIs) التوجيه القائم على البيانات، من مكتبة عميل قاعدة البيانات المرنة. ما يزال التطبيق من اللازم أن يخبر قاعدة بيانات أي TenantId يستخدم الاتصال. يخبر TenantId نهج الأمان RLS عن الصفوف التي يجب تصفيتها، كونها لا تنتمي إلى المستأجرين الآخرين. قم بتخزين TenantId الحالي في SESSION_CONTEXT من الاتصال.

أحد البدائل لـSESSION_CONTEXT هو استخدام CONTEXT_INFO. لكن SESSION_CONTEXT هو خيار أفضل. يعد SESSION_CONTEXT أسهل في الاستخدام، فهو يقوم بإرجاع NULL افتراضياً، كما أنه يدعم أزواج المفتاح والقيمة.

Entity Framework

بالنسبة للتطبيقات التي تستخدم Entity Framework، فإن أسهل طريقة هي تعيين SESSION_CONTEXT داخل تجاوز ElasticScaleContext الموضح في التوجيه المعتمد على البيانات باستخدام EF DbContext. قم بإنشاء وتنفيذ أمر SqlCommand الذي يقوم بتعيين TenantId في SESSION_CONTEXT إلى shardingKey المحدد للاتصال. ثم أرجِع الاتصال بوساطة، من خلال التوجيه المعتمد على البيانات. بهذه الطريقة، ما عليك سوى كتابة التعليمات البرمجية مرة واحدة لتعيين 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;
    }
}
// ...

الآن يتم تعيين SESSION_CONTEXT تلقائياً باستخدام TenantId المحدد عندما يتم استدعاء 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

بالنسبة للتطبيقات التي تستخدم ADO.NET SqlClient، أنشئ وظيفة برنامج تضمين حول أسلوب ShardMap.OpenConnectionForKey. اجعل برنامج التضمين يقوم تلقائياً بتعيين TenantId في SESSION_CONTEXT إلى TenantId الحالي قبل إعادة الاتصال. للتأكد من تعيين SESSION_CONTEXT دائماً، يجب فقط فتح الاتصالات باستخدام هذه الوظيفة للغلاف.

// 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. الطبقة المسؤولة عن البيانات: إنشاء نهج أمان على مستوى الصف

إنشاء نهج أمان لتصفية الصفوف التي يمكن لكل مستأجر الوصول إليها

الآن بعد أن قام التطبيق بتعيين SESSION_CONTEXT باستخدام TenantId الحالي قبل الاستعلام، يمكن لنهج أمان RLS تصفية الاستعلامات واستبعاد الصفوف التي لها TenantId مختلف.

يتم تطبيق RLS في Transact-SQL. تحدد وظيفة الوصول التي يحددها المستخدم منطق الوصول، ويربط نهج الأمان هذه الوظيفة بأي عدد من الجداول. بالنسبة لهذا المشروع:

  1. تتحقق الوظيفة من أن التطبيق متصل بقاعدة البيانات، وأن TenantId المخزن في SESSION_CONTEXT يطابق TenantId لصف معين.

    • التطبيق متصل، غير بعض مستخدمي SQL الآخرين.
  2. إن دالة تقييم FILTER، تتيح لجميع الصفوف التي تحقق متطلبات عامل التصفية TenantId للمرور خلال الاستعلامات SELECT، وUPDATE، وDELETE.

    • دالة التقييم BLOCK تمنع الصفوف التي تمنع عامل التصفية من أن يكون INSERTed أو UPDATEd.
    • إذا لم يتم تعيين SESSION_CONTEXT، ترجع الوظيفة NULL ولا توجد صفوف مرئية أو يمكن إدراجها.

لتمكين RLS على جميع القطع، نفّذ T-SQL التالي، باستخدام إما Visual Studio (SSDT)، أو، SSMS، أو البرنامج النصي PowerShell، المضمن في المشروع. أو إذا كنت تستخدم وظائف قاعدة البيانات المرنة، يمكنك أتمتة تنفيذ هذا T-SQL، على جميع القطع.

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

تلميح

في مشروع معقد قد تحتاج إلى إضافة دالة تقييم لمئات من الجداول، والذي من الممكن أن يكون أمراً مملاً. هناك إجراء مخزّن مساعد، ينشئ نهج الأمان تلقائياً، ويضيف دالة التقييم إلى جميع الجداول الموجودة في المخطط. للحصول على مزيدٍ من المعلومات، راجع منشور المدونة على تطبيق الأمان على مستوى الصفوف على جميع الجداول - برنامج نصي مساعد (مدوّنة).

الآن إذا قمت بتشغيل نموذج التطبيق مرة أخرى، فلن يرى المستأجرون إلا الصفوف التي تنتمي إليهم. بالإضافة إلى ذلك، لا يمكن أن يُدرج التطبيق صفوفاً تنتمي إلى المستأجرين، غير المستأجر المتصل في الوقت الراهن بقاعدة بيانات القطعة. علاوة على ذلك، لا يمكن للتطبيق تحديث TenantId في أي صفوف يمكنه رؤيتها. إذا حاول التطبيق القيام بأي من الأمرين، يتم رفع DbUpdateException.

إذا قمت بإضافة جدول جديد لاحقاً، ALTER نهج الأمان لإضافة دوال التقييم FILTER، و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

إضافة قيود افتراضية لملء TenantId تلقائياً لـ INSERTs

يمكنك وضع قيد افتراضي على كل جدول لتعبئة TenantId تلقائياً بالقيمة المخزنة حالياً في SESSION_CONTEXT عند إدراج الصفوف. وفيما يلي مثال على ذلك.

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

الآن التطبيق لا يحتاج إلى تحديد TenantId عند إدراج الصفوف:

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

ملاحظة

إذا كنت تستخدم القيود الافتراضية لمشروع Entity Framework، فمن المستحسن ألا تقم بتضمين العمود TenantId في نموذج بيانات EF الخاص بك. هذه التوصية لأن استعلامات Entity Framework توفر تلقائياً القيم الافتراضية التي تتجاوز القيود الافتراضية المُنشأة في T-SQL والتي تستخدم SESSION_CONTEXT. لاستخدام القيود الافتراضية في عينة المشروع، على سبيل المثال، يجب إزالة TenantId من DataClasses.cs (وتشغيل Add-Migration في وحدة تحكم إدارة الحِزَم) واستخدام T-SQL للتأكد من أن الحقل موجود فقط في جداول قاعدة البيانات. وبهذه الطريقة، يقوم EF تلقائياً بتوفير قيم افتراضية غير صحيحة، عند إدراج البيانات.

(اختياري) تمكين superuser من الوصول إلى كل الصفوف

قد ترغب بعض التطبيقات في إنشاء superuser، الذي يمكنه الوصول إلى كل الصفوف. بإمكان المستخدم الفائق تمكين إعداد التقارير عبر جميع المستأجرين على جميع القطع. أو بإمكان المستخدم الفائق إجراء عمليات دمج وتقسيم على القطع، التي تتضمن نقل صفوف المستأجر بين قواعد البيانات.

لتمكين المستخدم الفائق، أنشئ مستخدم SQL جديد، (superuser في هذا المثال)، في كل قاعدة بيانات خاصة بالقطعة. ثم غيّر نهج الأمان باستخدام دالة تقييم جديدة، تتيح لهذا المستخدم بالوصول إلى جميع الصفوف. ومثل هذه الدالة، يتم تحديدها بعد ذلك.

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

الصيانة

  • إضافة قطع جديدة: تنفيذ البرنامج النصي T-SQL لتمكين RLS على أية قطع جديدة، وإلا لن يتم تصفية الاستعلامات على هذه القطع.
  • إضافة جداول جديدة: إضافة دالة تقييم FILTER وBLOCK إلى نهج الأمان إلى كل القطع، كلما تم إنشاء جدول جديد. فيما عدا ذلك، لن تتم تصفية الاستعلامات الموجودة في الجدول الجديد. يمكن أن تكون هذه الإضافة تلقائية باستخدام مُشغّل DDL، على النحو الموضح في تطبيق الأمان على مستوى الصف تلقائياً على الجداول التي تم إنشاؤها حديثاً (مدونة).

الملخص

يمكن استخدام أدوات قاعدة البيانات المرنة، والأمان على مستوى الصف، معاً، لتوسيع الطبقة المسؤولة عن البيانات الخاصة بالتطبيق، مع دعم كل من القطع متعددة المستأجرين والمستأجر الواحد كليهما. يمكن استخدام القطع متعددة المستأجرين، لتخزين البيانات بكفاءة أكبر. وتتجلى هذه الكفاءة عندما يكون لدى عدد كبير من المستأجرين عدد قليل من صفوف البيانات. يمكن أن تدعم القطع ذات المستأجر الواحد، المستأجرين المتميزين، الذين لديهم متطلبات أداء وعزل أكثر صرامة. للحصول على مزيدٍ من المعلومات، راجع مرجع الأمان على مستوى الصف.

الموارد الإضافية

الأسئلة وطلبات الميزات

للأسئلة، اتصل بنا على صفحة أسئلة Microsoft Q&A لقاعدة بيانات SQL. ويمكنك إضافة أي طلبات ميزة إلى منتدى ملاحظات قاعدة بيانات لغة الاستعلامات المركبة (SQL).