我的编程空间,编程开发者的网络收藏夹
学习永远不晚

数据权限筛选(RLS)的两种实现介绍

短信预约 信息系统项目管理师 报名、考试、查分时间动态提醒
省份

北京

  • 北京
  • 上海
  • 天津
  • 重庆
  • 河北
  • 山东
  • 辽宁
  • 黑龙江
  • 吉林
  • 甘肃
  • 青海
  • 河南
  • 江苏
  • 湖北
  • 湖南
  • 江西
  • 浙江
  • 广东
  • 云南
  • 福建
  • 海南
  • 山西
  • 四川
  • 陕西
  • 贵州
  • 安徽
  • 广西
  • 内蒙
  • 西藏
  • 新疆
  • 宁夏
  • 兵团
手机号立即预约

请填写图片验证码后获取短信验证码

看不清楚,换张图片

免费获取短信验证码

数据权限筛选(RLS)的两种实现介绍

数据权限筛选(RLS)的两种实现介绍

 

在应用程序中,尤其是在统计的时候, 需要使用数据权限来筛选数据行。 简单的说,张三看张三部门的数据, 李四看李四部门的数据;或者员工只能看自己的数据, 经理可以看部门的数据。这个在微软的文档中叫Row Level Security,字面翻译叫行级数据安全,简称RLS。


要实现RLS, 简单的思路就是加Where条件语句来做数据筛选。但是必须是先Where, 也就是在其他Where条件和OrderBy、Fetch Rows 之前执行, 否则会对 排序、分页查询造成影响。这是一个难点。
另一个难点是如何对现有的业务代码侵入性降到最低——不影响现有查询逻辑的写法,甚至当需要的时候,可以关闭RLS。为了校验数据, 必须保持RLS开关的灵活性,尤其是在开发阶段。

下面介绍我在项目中使用过的两种实现方式。

数据权限筛选(RLS)的实现(一) -- Security Policy方式实现
这个主要参考微软的官文介绍实现, 分三个步骤, a. 定义Predicate函数, 根据user参数来筛选数据, b. 定义Security Policy, 使用前面指定的Predicate函数, c.在指定表上应用Security Policy。
其中的user, 一种是通过当前连接数据库的登录用户来获取,一种是通过exec sp_set_session_context @key=N"userId", @value=@userId 来传入用户。后者更适合我们在应用查询中使用统一的连接字符串。由于我们数据访问层是通过EF来实现的, 所以我们统一在自定义的DbContext类型中做了改造:

 1 public abstract class RlsDbContext : DbContext
 2 {
 3 
 4     protected readonly IUserProvider userProvider;
 5     protected RlsDbContext(
 6     string connectionString,
 7     IUserProvider userProvider)
 8     : base(options)
 9     {
10         this.connectionString = connectionString;
11         this.userProvider = userProvider;
12     }
13 
14     protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
15     {
16         connection = new SqlConnection(connectionString);
17         if (enableRLS)
18         {
19             connection.StateChange += Connection_StateChange;
20         }
21 
22         if (!enableMemoryDb)
23         {
24             optionsBuilder.UseSqlServer(connection);
25         }
26 
27         base.OnConfiguring(optionsBuilder);
28     }
29 
30     private void Connection_StateChange(object sender, System.Data.StateChangeEventArgs e)
31     {
32         if (e.CurrentState == ConnectionState.Open)
33         {
34             string userId = userProvider.CurrentUserId;
35             //此处判断条件用于流程Hook接口未配置认证而获取不到用户的情况
36             if (!string.IsNullOrEmpty(userId))
37             {
38                 SqlCommand cmd = connection.CreateCommand();
39                 cmd.CommandText = @"exec sp_set_session_context @key=N"userId", @value=@userId";
40                 cmd.Parameters.AddWithValue("@userId", userId);
41                 cmd.ExecuteNonQuery();
42             }
43         }
44         else if (e.CurrentState == ConnectionState.Closed)
45         {
46             //暂时注释:在分页查询场景下存在RLS获取总数之前SQL连接关闭的情况
47             //connection.StateChange -= Connection_StateChange;
48         }
49     }
50 
51 }

 

这样, 我们就能确保在访问数据库的适合, 传入了当前用户信息

具体的示例, 可以参考《Row-Level Security》
但是这个方式有个很大的问题, 就是性能不理想, 尤其是在判断条件中有or逻辑的时候。 比如这个场景:每个部门只能看自己的数据,如果是数据管理员,不论在哪个部门, 可以看所有部门的数据。加了or逻辑后, 大概1w行数据查询需要10s钟,这超出了应用能接收的范围。示例Predicate Function如下

 1 CREATE FUNCTION [dbo].[Predicate_MyFilter_RLS]
 2 (
 3     @orgId nvarchar(200)
 4 )
 5 RETURNS TABLE
 6     WITH SCHEMABINDING
 7 AS
 8 RETURN
 9    SELECT TOP 1 1 AS AccessPredicateResult
10    FROM dbo.[User] a
11    WHERE
12        a.UserId = SESSION_CONTEXT(N"UserId")
13     AND
14       (
15         a.OrgId = @orgId OR a.OrgId = "0000000000000000000000"
16       )
17 GO

 


关于性能问题的佐证,可以参考《Row-Level Security for Middle-Tier Apps – Using Disjunctions in the Predicate》

由于性能问题的障碍, 所以我们放弃了这种实现方式。但是这种方式比较优雅的满足了上述的两个条件,即实现了底层数据先筛选的逻辑,也对业务查询方法无侵入。在简单的场景中,应该是一款适合的方案。

 

数据权限筛选(RLS)的实现(二) -- 后台RlsStrategy方式实现
另一种做法, 是我们自行研究的RlsStrategy的实现方式。首先我们了解下接口IRlsStragety

 1 public interface IRlsStragety
 2 {
 3     Expressionbool>> UserPredicate
 4     {
 5         get;
 6     }
 7 
 8     Expressionobject>> OuterKeySelector
 9     {
10         get;
11     }
12 
13     Expressionobject>> InnerKeySelector
14     {
15         get;
16     }
17 
18     bool Skip();
19 }

 

这里面提供了三个表达式和一个bool 方法判断是否要略过RLS筛选。
下面是一个基本的实现:

 1 public class GenericUserOrgRlsStragety : IRlsStragety
 2 where TEntity : class, IUserId
 3 where TOrgUser : class, IOrgUser
 4 {
 5     private readonly IOrgProvider userOrgProvider;
 6     public GenericUserOrgRlsStragety(IOrgProvider userOrgProvider)
 7     {
 8         this.userOrgProvider = userOrgProvider;
 9     }
10 
11     public virtual Expressionbool>> UserPredicate
12     => user => user.OrgId == userOrgProvider.CurrentUserOrgId;
13 
14     public virtual Expressionobject>> OuterKeySelector
15     => entry => entry.UserId;
16 
17     public virtual Expressionobject>> InnerKeySelector
18     => user => user.UserId;
19 
20     public virtual bool Skip()
21     {
22         return false;
23     }
24 }

 

下面我来解释下这个逻辑。 假设应用中有这样两张表
T_BizData(Id, BizAmount, Org) 和T_OrgUser(Org, User), 前者是业务表, 记录了业务数据和所属业务组织的机构,后者是机构人员表,记录了人员和机构之间的关系。 根据这两个表,我们可以实现OrgA的用户可以查看OrgA的数据, OrgB的用户可以查看OrgB的数据

如果不考虑RLS, 则查询语句是 

Select * from T_BizData

 


如果考虑RLS, 则查询语句是

Select a.* from T_BizData a
   inner join T_OrgUser b on a.Org=b.org
where b.User=@user

 

两者比较,我们发现多了一个限制表和三处灵活点:
1 限制表就是 inner join T_OrgUser b,
2 灵活点 a) 取左表属性; b)取右表属性; c)取右表条件判断

这三个灵活点就是我们接口定义的三个表达式, 限制表是作为泛型类型传入进来的。

理解了这一点, 我们就可以看看下面这个代码

 1         public static IQueryable FilterByUser(
 2                 this IQueryable queryable,
 3                 TDbContext dbContext,
 4                 IRlsStragety rlsStragety
 5                 )
 6         where TDbContext : DbContext
 7         where TEntity : class
 8         where TUserConstraintEntity : class, IUserId
 9         {
10             if (dbContext is null)
11             {
12                 throw new System.ArgumentNullException(nameof(dbContext));
13             }
14 
15             if (rlsStragety == null
16                 || rlsStragety.UserPredicate == null
17                 || rlsStragety.OuterKeySelector == null
18                 || rlsStragety.InnerKeySelector == null
19                 || rlsStragety.Skip()
20                 )
21             {
22                 return queryable;
23             }
24 
25             
26             IQueryable result = queryable.Join(
27                        dbContext.Set()
28                                 .Where(rlsStragety.UserPredicate)
29                      , rlsStragety.OuterKeySelector
30                      , rlsStragety.InnerKeySelector
31                      , (p, q) => p
32                    );
33             return result;
34         }

 

我们都知道queryable 是EF实现查询的对象,它描述了查询的过程,所以我们在原queryable对象的基础上扩充了join逻辑, 从而实现了类似sql 语句的两表inner join查询。 该过程是在分页之前加入的,这样才能保证查询的结果。

 1         public virtual async Task> GetPagedListAsync(object filter, CancellationToken cancellationToken = default) where TEntity : class
 2         {
 3             if (filter == null)
 4             {
 5                 filter = new object();
 6             }
 7             IPaged result = new Paged();
 8 
 9             IQueryable queryable = GetPagedQueryable(filter);
10             result.Rows = await queryable.ToListAsync(cancellationToken).ConfigureAwait(false);
11 
12             IQueryable queryableForCount = GetCountQueryable(filter);
13             result.Total = await queryableForCount.CountAsync(cancellationToken).ConfigureAwait(false);
14 
15             return result;
16         }

 


以上准备工作做好了, 在查询的时候,就可以这样写了:

stragety =
serviceProvider.GetService();

var pageList = await rlsDataInquirer.GetPagedListAsync(filter, stragety);

 

最后, 补充下skip()方法的逻辑。

        public override bool Skip()
        {
            string orgId = userOrgProvider.CurrentUserOrgId;

            // 如果是信息管理部则跳过关联判断
            return orgId.Equals(InfoSupervisorDepartmentOrgId, StringComparison.CurrentCultureIgnoreCase);
        }

 

我们看到,FilterByUser方法的第19行, 如果skip()返回为true, 则会跳过RLS的逻辑。这个主要是为了特殊处理高级管理权限设计的。

 

总结:

     使用Security Policy 除了可以过滤用户权限数据外, 还可以用于更新和删除数据时的权限检查; 而使用RlsStrategy则只能基于现有的框架来实现查询数据行时的筛选,但是性能上要好很多,而且也比较灵活。同时,因为底层是转换成了SQL语句,所以对字段加索引应该可以进一步提高查询的性能。

 

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

数据权限筛选(RLS)的两种实现介绍

下载Word文档到电脑,方便收藏和打印~

下载Word文档

猜你喜欢

数据权限筛选(RLS)的两种实现介绍

在应用程序中,尤其是在统计的时候, 需要使用数据权限来筛选数据行。 简单的说,张三看张三部门的数据, 李四看李四部门的数据;或者员工只能看自己的数据, 经理可以看部门的数据。这个在微软的文档中叫Row Level Security,字面翻译叫行级数据安全,简称
数据权限筛选(RLS)的两种实现介绍
2018-05-06

mysql between实现选取介于两个值之间的数据范围

目录BETWEEN 实例BETWEEN 时间日期BETWEEN 数据ZaRFpmfMO比较mysql IN 用法1.IN 运算符用于WHERE 表达式中2.IN 子查询3.IN 运算符补充说明4.关于 IN 运算符的效率问题between和
2022-07-03

mysql between如何实现选取介于两个值之间的数据范围

本文小编为大家详细介绍“mysql between如何实现选取介于两个值之间的数据范围”,内容详细,步骤清晰,细节处理妥当,希望这篇“mysql between如何实现选取介于两个值之间的数据范围”文章能帮助大家解决疑惑,下面跟着小编的思路
2023-07-02

编程热搜

目录