Skip to content

数据权限

常规的角色-菜单(按钮)机制控制了用户可以使用哪些功能,但有时我们需要更进一步,控制用户对数据的访问范围。比如,企业通讯录功能,A用户能查看全部数据,而B用户只能查看其所在部门的数据。于是我们实现了数据权限相关的功能。

概述

数据维度

对于某一项数据,我们可能想从多个方面限定访问范围。例如工单数据的责任部门、客户地区、客户等级, 这样可以灵活地控制员工能查看哪些部门、哪些地区、哪些等级的客户。 我们把这些需要控制其可见范围的数据字段称为数据维度。

功能与数据

系统中的页面功能各种各样,涉及的数据也不尽相同,因此每个页面都可以有自己的数据维度。当然,一个数据维度也可能在多个页面功能中使用。

用户权限

对于同一个数据维度,用户在不同页面的权限很有可能是不同的。比如组织机构,用户在查询页面可以使用整个部门的数据,但在录入页面却只能使用自己小组的数据。因此需要分别设置用户在每个页面每个维度的权限。

提示

为什么不把数据权限和角色绑定?

角色与功能通常是一对多的关系,如果将角色与数据权限绑定,就意味着这些功能都使用相同的数据权限。这与之前考虑的不同功能需要不同的数据权限就产生了冲突,因此将数据权限独立管理,可以实现更高的灵活性。

到这里,我们得出一个基本结论——数据权限是一个人员-页面-数据维度的组合关系。

维度管理

要想可视化地为用户授权,就必须要将数据维度和其对应的选项显示出来。因此我们需要维度管理功能,来维护系统内可用的数据维度。

其中关键的一步就是要设置每个维度的数据源。 例如,责任部门通常使用系统的组织机构树,客户地区和等级使用系统字典。有时还需要一些更细节的配置,例如是否允许多选,树数据是否仅允许选择叶子节点等。

scope item

提示

  1. 数据源支持字典(含系统字典和业务字典)、接口类型。
  2. 显示模式支持单选框、复选框、树。
  3. 数据源类型为字典时,“数据源”填写字典名;类型为接口时,“数据源”填写 URL(仅支持 Get 请求)。
  4. 文本字段和值字段,决定了实际存储的和显示给用户的内容。

特殊维度

上面提到的维度,取值范围都比较清晰,数据是确定且可以预知的。 但有些时候却并不方便获得,比如要限制只能查看当前用户自己创建的数据,或者下属创建的数据。 这些选项是动态的,无法提前预设为选项。这时需要使用特殊的选项来达到目的。

对于创建人这个维度,我们可以设置为包含“我自己”、“所有人”这两个值的字典。 在后端执行数据过滤逻辑时,再根据这些选项应用对应的过滤条件即可。这也就是说,用户获得授权的维度可用值(例如“我自己”)此时不能直接作为过滤条件的值,要经过一次转换才能使用。

页面与维度

有了数据维度,还需要将它们与功能页面绑定起来,为下一步用户授权做好准备。 在“系统设置”-“菜单管理”功能中,我们加入了数据维度选项,可以方便地指定某个功能页面使用的数据维度。 scope menu

用户授权

用户授权功能,顾名思义就是为用户分配可以使用的具体数据权限。进入“数据权限”-“用户权限”功能,授权界面如下图所示。

user scope

左侧部分为目标用户所拥有的角色和对应的系统菜单,有多个角色时可以通过顶部下拉框切换角色。点击菜单后,右侧出现与之绑定的数据维度,同时显示每个维度可用的选项。维度管理中配置的数据源和显示模式就在这里体现。

如果有多个页面需要授权,可以依次点击菜单设置权限,最后再统一保存。 保存之后已授权数据会显示在列表中,如下图所示。

use scope list

复制授权

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

copy

提示

默认仅复制当前页面(复制按钮所在位置)的数据权限,也可以根据需要切换为全部页面。

代码集成

前端

前端框架会将每个页面对应的标识(pageId)放入路由元数据(meta)中,开发人员需要将页面标识传入后端以区分用户正在使用哪个页面。

javascript
// 初始化页面权限查询参数
initPagePermission() {
  if (this.$route.meta.pageId) {
    this.query.pageId = this.$route.meta.pageId
  }
}

后端

  1. 添加注解

服务方法添加@DataScope注解,参数如下表所示。

参数类型说明
scopesList<String>声明需要控制的数据维度。
pageStringEL表达式,声明页面标识参数的位置。
  1. 声明形参

服务方法应声明scopes形参,由框架注入当前操作人员的权限集。

  1. 筛选数据

服务方法根据权限集,拼接ORM查询条件,筛选数据。

java
@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 差异

前端

前端无需向后端传值。

后端

  1. 后端在控制器方法添加[DataScope]特性,参数如下表所示。
参数类型说明
PageUrlString声明来源页面的 URL,需要与系统菜单配置的访问 URL 一致。
ScopesString数据维度标识,多个以逗号分隔。
csharp
[HttpGet]
[DataScope("/work/list", "scp_creator,scp_org")]
public Result ListFoo(FooDto dto)
{
    service.ListFoo(dto);
    return Result.Success();
}

提示

方法执行时,用户实际授权的数据集会放入 UserDataScopeBo 对象,可以在请求作用域内使用。

  1. 服务方法根据权限集,拼接ORM查询条件,筛选数据。
csharp
[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);
}