天天看点

SaaS 多租户架构持久层的设计(四)SQL数据库的数据隔离

作者:芝士Flash

之前有朋友提到数据的隔离,这里先介绍一种传统SQL的隔离:RLS的隔离。

SaaS 多租户架构持久层的设计(四)SQL数据库的数据隔离

1. 应用层:在SESSION_CONTEXT中设置 TenantId

首先,使用数据库客户端库的数据相关路由 API 连接到分片数据库。应用程序仍必须告知数据库哪个 TenantId 正在使用连接。TenantId 告知 RLS 安全策略哪些行必须筛选为属于其他租户。将当前租户 ID 存储在连接的SESSION_CONTEXT中。

SESSION_CONTEXT的替代方法是使用CONTEXT_INFO。但SESSION_CONTEXT是更好的选择。SESSION_CONTEXT更易于使用,但它默认返回 NULL,并且支持键值对。zai

在返回connection前把SESSION_CONTEXT附加到connection上:

cmd.CommandText =
            @"exec sp_set_session_context
                @key=N'TenantId', @value=@shardingKey";
        cmd.Parameters.AddWithValue("@shardingKey", shardingKey);
        cmd.ExecuteNonQuery();           

2. 数据层:创建行级别安全策略

创建安全策略以筛选每个租户可以访问的行

现在,应用程序在查询之前使用当前 TenantId 设置SESSION_CONTEXT,RLS 安全策略可以筛选查询并排除具有不同 TenantId 的行。

RLS 在 Transact-SQL 中实现。用户定义的函数定义访问逻辑,安全策略将此函数绑定到任意数量的表。对于此项目:

  1. 该函数验证应用程序是否已连接到数据库,以及存储在SESSION_CONTEXT中的 TenantId 是否与给定行的 TenantId 匹配。
  2. 应用程序已连接,而不是其他某个 SQL 用户。
  3. 筛选器谓词允许满足 TenantId 筛选器的行通过选择、更新和删除查询。
  4. BLOCK 谓词可防止未通过筛选器的行入或更新。
  5. 如果尚未设置SESSION_CONTEXT,则该函数返回 NULL,并且没有行可见或无法插入。

若要在所有分片上启用 RLS,可以看一下此 T-SQL。创建一个policy 去过滤数据。

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           

如果稍后添加新表,请更改安全策略以在新表上添加 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           

添加默认约束以自动填充插入的租户 ID

可以在每个表上放置默认约束,以便在插入行时使用当前存储在SESSION_CONTEXT中的值自动填充 TenantId。下面是一个示例。

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

继续阅读