前言:“我们有一个订单列表,希望能够根据当前登陆的不同用户看到不同类型的订单数据”、“我们希望不同的用户能看到不同时间段的扫描报表数据”、“我们系统需要不同用户查看不同的生产报表列”。诸如此类,最近经常收到项目上面的客户提出的这种问题,即所谓的“数据权限”,经过开会讨论决定:在目前的开发框架上面搭建一套通用的数据权限功能。
本文原创地址:http://www.cnblogs.com/landeanfen/p/7760803.html
一、大话权限模块
有了上面的引言,自然而然就引出了今天需要和大家讨论的话题——数据权限。作为开发人员,我们肯定知道,一般的系统都离不开权限模块,它是支撑整个系统运行的基础模块。而根据项目类型和需求的不同,权限模块的设计更是大相径庭。但不管怎么变,权限模块从大的方面来说,可以分为两种大的类型:功能权限 和 数据权限。
- 功能权限:主要控制不同的资源主体(用户、角色、组织等)有操作不同的资源的权限。比如常见的不同的角色能访问不同的页面(菜单权限),以及具有操作同一页面的不同功能(按钮权限)等等,都数据功能权限的范畴,这种设计相对比较简单,也比较为大多数系统所通用。当然网上资料、设计思路也可以找到很多。
- 数据权限:主要控制不同的资源主体(用户、角色、组织等)有查看不同的数据信息的权限,一般来说,数据权限又分为数据行权限和数据列权限,通过字面意思不难理解这两者的区别,比如上文“我们有一个订单列表,希望能够根据当前登陆的不同用户看到不同类型的订单数据” 这就是一个典型的数据行权限,而“我们系统需要不同用户查看不同的生产报表列”这就是数据列权限的范畴。由于数据权限和系统的业务逻辑关系非常密切,所以不同的系统设计差异性会非常大。从另一方面来说,由于数据权限和业务逻辑关联性非常强,如果系统的业务逻辑非常复杂,数据权限设计起来也会相对复杂,所以关于数据权限的设计一直没有一种相对通用和使用简单的设计方案。
如果你动手去设计数据权限,当你去各大平台、百度、谷歌查找设计思路的时候,你会发现很难找到有用的资料,很多设计思路局限性非常大。其实原因很简单:数据权限的设计和他人系统关系紧密,一般不太容易拿到你的项目上面直接使用。
当然也有另外部分人说“数据权限并不能作为权限模块去设计”,比如博主看到这样一条评论
从一定程度上来说,这样理解也不为过,如果你觉得你的系统灵活性和配置性不需要那么高,把数据权限的规则在代码里面写死又何妨,博主所在公司的另外一个部门就是这么干的,除了编码量大一点,其实也没什么太大的问题!其实博主说这么多无非是想表达一个观点:没有绝对通用的数据权限设计思路,关键看合不合你用!当然本文的设计思路也是一样,不强制要求,不提通用。设计思路供你参考!
二、一种设计思路
关于权限设计的杂谈就告一段落,凡是点到即止,再说了多了就说烂了。到目前为止,博主找到的一篇写得相对比较好的文章 通用权限管理设计 之 数据权限。这位博主是用sql去实现的,如果是把这个运用到EF里面的话,考虑到EF复杂的导航属性,会有一些问题。接下来说说博主这边想到的设计思路。
先说说博主所在项目的情况,和数据打交道的部分采用EntityFramework+Repository的传统模式去实现的,整个项目从上到下,就是一种典型的"伪DDD",什么是”伪DDD“?这里不做过多说明,使用过DDD的同仁应该很清楚。下面是设计思路流程图:
第一步:配置数据规则
第二步:页面使用数据规则
以上是一个大致的思路图,总的来说,要实现基于EF的数据权限设计,主要分为两大步骤
1、配置数据规则
配置数据规则这里有三个大的方面:功能模块、数据资源、角色
- 功能模块:为什么这里要加上功能模块的约束?是因为博主觉得我们某一个页面在查询数据的时候,会有一个查询的范围,比如订单查询页面肯定只能查询和订单有关联的实体功能,而不可能查询和它没有任何关联的业务。加这个约束更大的意义在于我们动态的构造Lambda去查询实体的时候不会产生”找不到相关联的实体“之类的错误。
- 数据资源:具体对哪种数据资源做数据权限,比如订单的状态不等于取消状态、订单的下单时间小于当前月份等等。
- 角色:数据资源的主体,还可以是用户、部门、组织等等。
这三者配置之后得到的一个结果就是某一类角色的某一个功能模块对哪个数据资源的数据规则是什么样的。比如有一条销售总监的数据规则,配置销售总监在订单模块里面订单这个实体的订单类型是销售订单的所有数据,这就是针对销售总监在订单模块的数据规则。可能最终数据库存储得到的数据类似这样:
RoleId | FunctionCode | Rules |
2 | OrderQuery |
{"rules":[{"field":"Order_Status","operate":"in","value":"[0,1,2]"},{"field":"Order_Type","op":"equal","value":"1"}],"logicoperate":"and"} |
3 | OrderQuery |
{"rules":[{"field":"Order_Status","operate":"in","value":"[0]"},{"field":"Product.Categary.Type","equal":"equal","value":"1"}],"logicoperate":"and"} |
5 | Product |
{"groups":[ {"rules":[{"field":"Order_Status","operate":"in","value":"[0,5,10]"},{"field":"Order_Type","op":"equal","value":"1 "}],"logicoperate":"and"}, {"rules":[{"field":"LineName","operate":"equal","value":"fenzhuangxian"}]} ],"logicoperate":"or"} |
需要特别说明的是:由于EF有导航属性,这里的Rules在保存的时候如果遇到导航属性,我们的字段值需要这样保存——Product.Categary.Type。因为在我们转换成为lambda表达式的时候导航属性会是这样写:x=>x.Product.Categary.Type==1。这个我们在后面使用这个规则的时候加以说明。
2、使用数据规则
有了上面的数据规则,接下来就是我们在取数据的时候如何使用了,这里有一点需要说明的是:我们这里需要传两个参数,一个是模块的名称,比如上面的OrderQuery、Product等;第二个是当前用户的角色id,这个可以通过当前登陆用户的id获取到角色。
要使用数据规则,之前博主分享过两篇关于动态Lambda的文章,现在派上用场了。只不过原来只是一些基础类型转lambda,现在涉及到了导航属性,不知道是否可行。博主查阅了一些资料,最终找到了解决方案。
//遍历得到属性(包括遍历导航属性) public Expression GetProperty(Expression source, ParameterExpression para, string Name) { string[] propertys = Name.Split('.'); if (source == null) { source = Expression.Property(para, typeof(Entity).GetProperty(propertys.First())); } else { source = Expression.Property(source, propertys.First()); } foreach (var item in propertys.Skip(1)) { source = GetProperty(source, para, item); } return source; }
然后测试如下
var oLamadaExtention = new LambdaExpression<Order>(); var left = oLamadaExtention.GetProperty(null, Expression.Parameter(typeof(Order), "x"), "Product.Categary.Type"); var value = Expression.Constant("1", left.Type); //动态转换类型 var right = Expression.Constant(value, left.Type); Expression expRes = Expression.Equal(left, right);
测试得到的查询lambda结果为x=>x.Product.Categary.Type=="1",测试成功!
3、补充一点
对于配置数据规则的时候还有一点比较麻烦的是,如果如何知道哪个功能模块使用哪些实体?不可能直接让用户去写Product.Categary.Type这些复杂的功能吧,如果是这样,谈何体验。那么只有使用另外一种解决思路了——反射EF实体。
反射EF实体的时候如果是导航属性,还得继续反射导航属性的实体,这样一层一层反射下去,最终确实是可以得到形如Product.Categary.Type这个的结构体,但界面如何展现还有待思考。比如思路如下:
三、总结
以上只是一个设计思路,理论上来说是可以实现的,如有不足,欢迎斧正,谢谢。如果思路没有问题,后续博主会抽时间将这种设计的实现过程展现出来供大家参考,欢迎关注。其中的难点有两个:
1、逐级反射EF的导航属性,以及这个过程如何展现。是通过特性标记,还是开发人员配置;
2、动态Expression在构造Lambda的时候和配置数据的兼容性问题,比如数据类型的兼容性有点难控制。
本文原创出处:http://www.cnblogs.com/landeanfen/
欢迎各位转载,但是未经作者本人同意,转载文章之后必须在文章页面明显位置给出作者和原文连接,否则保留追究法律责任的权利