数据权限
常规的角色-菜单(按钮)机制控制了用户可以使用哪些功能,但有时我们需要更进一步,控制用户对数据的访问范围。比如,企业通讯录功能,A用户能查看全部数据,而B用户只能查看其所在部门的数据。于是我们实现了数据权限相关的功能。
概述
数据维度
对于某一项数据,我们可能想从多个方面限定访问范围。例如工单数据的责任部门、客户地区、客户等级, 这样可以灵活地控制员工能查看哪些部门、哪些地区、哪些等级的客户。 我们把这些需要控制其可见范围的数据字段称为数据维度。
功能与数据
系统中的页面功能各种各样,涉及的数据也不尽相同,因此每个页面都可以有自己的数据维度。当然,一个数据维度也可能在多个页面功能中使用。
用户权限
对于同一个数据维度,用户在不同页面的权限很有可能是不同的。比如组织机构,用户在查询页面可以使用整个部门的数据,但在录入页面却只能使用自己小组的数据。因此需要分别设置用户在每个页面每个维度的权限。
提示
为什么不把数据权限和角色绑定?
角色与功能通常是一对多的关系,如果将角色与数据权限绑定,就意味着这些功能都使用相同的数据权限。这与之前考虑的不同功能需要不同的数据权限就产生了冲突,因此将数据权限独立管理,可以实现更高的灵活性。
到这里,我们得出一个基本结论——数据权限是一个人员-页面-数据维度的组合关系。
维度管理
要想可视化地为用户授权,就必须要将数据维度和其对应的选项显示出来。因此我们需要维度管理功能,来维护系统内可用的数据维度。
其中关键的一步就是要设置每个维度的数据源。 例如,责任部门通常使用系统的组织机构树,客户地区和等级使用系统字典。有时还需要一些更细节的配置,例如是否允许多选,树数据是否仅允许选择叶子节点等。

提示
- 数据源支持字典(含系统字典和业务字典)、接口类型。
- 显示模式支持单选框、复选框、树。
- 数据源类型为字典时,“数据源”填写字典名;类型为接口时,“数据源”填写 URL(仅支持 Get 请求)。
- 文本字段和值字段,决定了实际存储的和显示给用户的内容。
特殊维度
上面提到的维度,取值范围都比较清晰,数据是确定且可以预知的。 但有些时候却并不方便获得,比如要限制只能查看当前用户自己创建的数据,或者下属创建的数据。 这些选项是动态的,无法提前预设为选项。这时需要使用特殊的选项来达到目的。
对于创建人这个维度,我们可以设置为包含“我自己”、“所有人”这两个值的字典。 在后端执行数据过滤逻辑时,再根据这些选项应用对应的过滤条件即可。这也就是说,用户获得授权的维度可用值(例如“我自己”)此时不能直接作为过滤条件的值,要经过一次转换才能使用。
页面与维度
有了数据维度,还需要将它们与功能页面绑定起来,为下一步用户授权做好准备。 在“系统设置”-“菜单管理”功能中,我们加入了数据维度选项,可以方便地指定某个功能页面使用的数据维度。 
用户授权
用户授权功能,顾名思义就是为用户分配可以使用的具体数据权限。进入“数据权限”-“用户权限”功能,授权界面如下图所示。

左侧部分为目标用户所拥有的角色和对应的系统菜单,有多个角色时可以通过顶部下拉框切换角色。点击菜单后,右侧出现与之绑定的数据维度,同时显示每个维度可用的选项。维度管理中配置的数据源和显示模式就在这里体现。
如果有多个页面需要授权,可以依次点击菜单设置权限,最后再统一保存。 保存之后已授权数据会显示在列表中,如下图所示。

复制授权
也许你已经注意到了,上面介绍的方式一次只能对一个人设置权限。如果有多个人员权限相似,逐一操作会非常麻烦。因此我们提供了授权复制功能,可以将某个人员的权限复制给其他人员。

提示
默认仅复制当前页面(复制按钮所在位置)的数据权限,也可以根据需要切换为全部页面。
代码集成
前端
前端框架会将每个页面对应的标识(pageId)放入路由元数据(meta)中,开发人员需要将页面标识传入后端以区分用户正在使用哪个页面。
// 初始化页面权限查询参数
initPagePermission() {
if (this.$route.meta.pageId) {
this.query.pageId = this.$route.meta.pageId
}
}后端
- 添加注解
服务方法添加@DataScope注解,参数如下表所示。
| 参数 | 类型 | 说明 |
|---|---|---|
| scopes | List<String> | 声明需要控制的数据维度。 |
| page | String | EL表达式,声明页面标识参数的位置。 |
- 声明形参
服务方法应声明scopes形参,由框架注入当前操作人员的权限集。
- 筛选数据
服务方法根据权限集,拼接ORM查询条件,筛选数据。
@DataScope(scopes = {"dev_type","creator"}, page="#p0.pageId")
getList(FooAddDto input, Map<String, List<String>> scopes) {
// 业务侧应用维度筛选
service.list(new QueryWraper<Entity>().lambda()
.in(CollectionUtils.isNotEmpty(scopes.get("dev_type"), Entity::getDeviceTypeId, scopes.get("dev_type"))
.eq(CollectionUtils.isNotEmpty(scopes.get("creator"), Entity::getCreateUserId, SecurityUtils.getLoginUser().getUserId())
);
}提示
#p0代表第一个方法参数,即 input, #p0.pageId 表示取出其属性 pageId 的值。
若 pageId 通过单独的基本类型参数传入,例如 getList(FooAddDto input, String pageId,Map<String, List<String>> scopes), 则 page 表达式应为 #p1。
.NET 差异
前端
前端无需向后端传值。
后端
- 后端在
控制器方法添加[DataScope]特性,参数如下表所示。
| 参数 | 类型 | 说明 |
|---|---|---|
| PageUrl | String | 声明来源页面的 URL,需要与系统菜单配置的访问 URL 一致。 |
| Scopes | String | 数据维度标识,多个以逗号分隔。 |
[HttpGet]
[DataScope("/work/list", "scp_creator,scp_org")]
public Result ListFoo(FooDto dto)
{
service.ListFoo(dto);
return Result.Success();
}提示
方法执行时,用户实际授权的数据集会放入 UserDataScopeBo 对象,可以在请求作用域内使用。
- 服务方法根据权限集,拼接ORM查询条件,筛选数据。
[Autowired]
UserDataScopeBo scopes;
public List<FooVo> ListFoo(FooDto dto) {
// 业务侧应用维度筛选
var where = new OptionalExpressionBuilder<Func<Foo, bool>>()
.Where(t => scopes["scp_org"].Contains(t.OrgId))
.WhereIf(scopes["scp_creator"][0] == "me", t => t.UpdateUser == me.Id)
.Build();
return SelectVoList<FooVo, string>(where, t => t.EmpSn, true, 1, 10);
}