基于EntityFramework的权限的配置和验证


1.   概要

本文主要介绍公司现有系统框架的权限体系,和一些待扩展功能的说明。目前该权限体系基于角色构建(RBAC),原则上,系统中不允许出现对用户、组织等其他对象指派权限的情况。

2.   权限分类

目前系统中的所有表现形式的权限可以归为两类:

一类是描述对象或动作是否可见,我们称之为功能权限(Authority)。例如,菜单的可见性、用户添加按钮是否可见、用户添加方法是否可用等;

另一类是描述可见对象的范围或动作的影响范围,我们称之为数据权限(Permission)。例如,未成年用户列表、行政区域内数据操作限制等。

说明,这里authority和permission并不是取英文原意,只是用于程序简化权限对象命名,实际对应的应该是Function Permission 和 Data Permission。

2.1. 功能权限

功能权限的定义通常都可以序列化存入数据库。因此,在设计数据库表时,它和用户、角色的关系是这样的:

权限和角色、角色和用户都是多对多的关系,需要建立中间表来存储。

2.2. 数据权限

数据权限的定义大多数情况下难以序列化存入数据库。因此,在设计数据库表时,只需要记录它和角色的关系即可:

3.   实现过程

从用户的角度来说,他们只关心系统配置好之后是否符合实际业务逻辑,少数用户会主动参与到配置过程。

3.1. 配置

配置主要做了两件事情,一是定义权限,二是授权。

功能权限和数据权限的授权并没有什么区别,从数据表来看,可以简单的理解为对Role2Authority和Role2Permission表的数据改动;从界面上来看,如图

菜单权限是功能权限的一种表现形式。

3.1.1.   功能权限的定义

而定义则有较大的不同,功能权限可以在没有确定管理对象的情况进行定义,而且从实际的情况来看,系统的功能或界面设定并不完全和系统管理对象一一对应,功能权限更多的情况是对系统需求的描述。

例如,我们计划在系统中增加工程对象管理功能,这时可以快速在Authorities表中加入一个菜单权限,用于控制用户是否可以访问工程对象管理页面project.html,而此时,project.html页面可能还未开发完成,但并不影响权限的定义。另一方面,该权限定义的动作中涉及的对象可能不止工程对象,而是由多个对象操作结合完成的操作;因此,功能权限的定义应该尽量按照用户需求最小单元来划分。

3.1.2.   数据权限的定义

动态定义

数据权限的定义,目前只实现了预定义,即编码阶段的定义。数据权限授权界面所看到的可选项均为编码预设的内容。

如果想要在运行时,动态定义数据权限,例如,项目权限中的部门项目,它的定义内容为当前登录用户可以查看本部门的项目,这是对项目对象的一个范围限制。使用SQL语句,可以描述为

SELECT * FROM Projects WHERE OrgId=(SELECT OrgId FROM Users WHERE Id=@UserId)

这里可以在数据库中建立一个Permissions表,将上面这条SQL语句的条件部分命名为部门项目,并给定编号3002,存入表中;然后建立Permissions表和Role2Permissions表的外键关系实现数据权限的动态配置化,但具体操作下来,会发现很多问题。

首先,联表查询时,条件语法是有变化的;其次,动态配置需要有配置界面,在界面上录入SQL语句是不安全的,而且用户很可能并不了解SQL语法;在一些管理权限框架中,也有使用条件表达式生成器简化定义过程,这是可以尝试的方向。

而本文描述的系统中,数据层采用的是EntityFramework框架,因此无法使用SQL语法来实现动态定义数据权限。或许在对lambda表达式有了足够了解后,可以尝试通过文本建立lambda表达式,从而实现动态定义数据权限。

预定义

对于部门项目权限系统采用的是lambda描述

Formula = oper => (pro => pro.Organizations.Contains(oper.Organization));

其中oper为用户参数,pro为项目参数。数据权限的预定义内容存放在类中,并为此建立新的类库项目,称为权限库。当系统需要新的数据权限时,可以只修改权限库,生成部署文件,替换运行时环境中的部分文件达到数据权限配置化的目的。

上述表达式Formula只是数据权限对象的一个属性。数据权限名称可以定义为部门项目,编号为3002,这样可以和Role2Permissions表中的PermissionId对应起来,实现对角色的授权。

3.2. 验证

3.2.1.   功能权限的验证

对于本文描述的B/S系统,功能权限的验证可分为服务端和客户端两个不同实现。

首先,服务端的实现需要拦截所有请求,并根据会话中用户的角色、请求的资源,逐一匹配权限定义规则,实现验证。

例如,所有Handler接口都继承自一个父类,并在父类中进行验证。

客户端的实现则是隐藏用户没有权限的菜单或按钮。对于使用后台脚本语言的页面,可以使用用户角色下的权限集合很方便的进行控制;而纯html前段,则可以考虑首先隐藏所有涉及权限的元素,再通过ajax获取权限集合,显示可以访问的元素。

需要注意的是,服务端验证是必须要实现,以保证权限系统的可靠性。

3.2.2.   数据权限的验证

与功能权限不同的是,数据权限主要是对数据范围的控制;因此,验证过程最终会在系统数据层实现。这里有两种实现方式:一是只读验证,另一种是读写验证,各自有优缺点。

只读验证的方式如下面这个方法:

这是一个API查询方法,其中condition、verCondtion、attCondition是针对三个对象的数据权限,通过这个查询方法,用户将只能得到他可以查看的Project对象及子对象数据。

类似的,分页查询、视图查询等查询方法,除了用户输入的条件,还需加入PermissionManager提供的数据权限条件,最终交给业务层查询。这种方式的缺点在于,用户可能使用查询权限以外的数据,伪造修改和删除操作。

读写验证除了查询方法需要添加额外权限条件,修改和删除也要先进行数据可读性查询验证。因此,相对而言,读写验证的安全性是最高的,但性能则要比只读查询差很多。

3.2.3.   一些细节

条件的合并,PredicateBuilder是专门用于lambda条件表达式合并的工具。通常数据权限条件和用户查询条件应该使用And操作。

集中管理数据权限的验证,上述在API方法中使用PermissionManager获取数据权限条件的方式是有一些缺点的,例如,这要求所有编码人员都了解权限验证的过程,也有遗漏的可能导致数据操作不受控制。对于使用EF框架的系统,可以考虑在DbContext中集成权限验证过程。

具体的验证过程,创建一个通用的验证方法,使用当前用户作为参数,返回相关对象的过滤条件。由于数据权限是建立在对象基础上的,因此该验证方法最好使用泛型。方法将分析用户的所有角色,以及每个角色下的数据权限授权编号,并根据授权编号从预定义库中取出所有相关lambda表达进行Or或者And组合。组合方式取决于多角色权限重合的处理策略。

4.   遗留问题

最后,本文虽然到此,但基于EF框架的权限开发还有一些问题尚未解决。例如,动态定义数据权限,权限读写验证的效率问题,数据权限的集中验证等,希望自己有时间继续完善。

本文仅在博客园发布,转载请注明出处。