Skip to content

数据权限

在数据权限构建的这场复杂而深刻的旅程中,作者经过了深思熟虑和不懈的探索。我们在这里展示的解决方案,虽然可能不是完美无瑕,但它代表了我们对问题的一种理解和尝试。我们希望这不仅仅是一个方案的展示,更是一个思考的起点。

同时,作者在实现数据权限的过程中,不禁提出了一个关键问题:"我们是否真的需要构建如此复杂的数据权限体系?这样的复杂性是否真正为我们带来了价值?" 。 我们鼓励每一位用户,带着这样的问题,来体验此功能。

读前须知

注意事项

  1. 同步场景优先:当前方案主要针对同步场景设计,以确保数据一致性和实时性。我们认识到并非所有场景都适用,因此将持续探索以适应更多数据交互模式。
  2. 乐观权限控制:我们采取了一种乐观的权限管理策略,即不是所有数据都需要进行严格的权限控制。我们专注于对那些确实需要保护的敏感数据实施权限管理。
  3. 半自动化实现:为确保业务逻辑的清晰性和减少操作歧义,我们采用了半自动化的实现方式。这种方法结合了配置的灵活性和编码的精确性,以实现特定数据的权限控制。
  4. 最小权限单位:我们以菜单(menu)而非数据库表(table)作为数据权限控制的最小单位。这样做避免了用户需要进行大量表配置操作的繁琐,提高了效率和实用性。
  5. 注解依赖性:核心功能依赖于@SaCheckPermission注解来实现。为了确保数据权限的正常运行,请确保所有需要实现权限控制的Controller层都正确配置了该注解。这将帮助系统自动识别并应用相应的权限规则。
  6. 数据归属规则:我们的数据归属规则基于数据创建时的用户状态。这意味着,当用户创建数据时,系统将根据用户当时的部门归属来标记数据。例如,如果张三同时属于财务部和市场部,并且他创建了一条数据,系统将记录这条数据的创建者为create_id="张三ID",并标注其部门范围为dept_scope="[财务部ID, 市场部ID]"。这样的设计确保了数据归属的明确性,并有助于实现跨部门的数据管理和权限控制。
  7. 最小范围规则:系统默认对(已开启但未配置具体权限的)菜单项限制用户访问,仅允许访问本人数据,以保障数据安全。

v2.0.0 当前口径

v2.0.0 已将旧版“独立数据权限角色/数据角色管理”的主流程合并到 系统角色授权 中。当前数据权限配置入口如下:

环节当前入口 / 表说明
开启菜单数据权限菜单管理中的“数据权限支持”开关,对应 sys_menu.use_data_scope只有菜单类型为页面菜单时才参与数据权限配置
给角色配置数据范围角色管理 -> 权限配置弹窗,对应 sys_role_menu.permission_type = scope同一个角色可以在不同菜单上配置不同数据范围
保存自定义范围sys_data_role_relation仅保存自定义数据权限的部门/用户关系,不再代表一个独立“数据角色”功能入口
请求权限识别Controller 方法上的 @SaCheckPermission后端根据权限标识找到菜单,再找到该菜单对应的数据范围
SQL 自动注入DataScopeSession / SimpleDataScopeHelper + MyBatis-Flex 方言只在业务查询显式开启数据权限上下文后生效

因此,日常使用时不需要再单独维护“数据角色菜单”。管理员应先在菜单管理中开启数据权限支持,再在系统角色的授权弹窗中为该菜单选择“全部、本部门及以下、仅本部门、仅本人、自定义”等范围。

内置规则

我们的系统内置了五种灵活的数据权限规则,以满足不同场景下的需求:

  • 全部:用户被授予查看所有数据的权限,适用于需要全面数据访问的角色。
  • 本部门及以下:用户可以查看自己所在部门及其所有子部门的数据。为实现这一规则,我们采用了闭包表结构(sys_dept_closure表),确保了查询的效率和速度。
  • 仅本部门:用户只能访问自己所在部门的数据,适用于需要限制数据访问范围的情况。
  • 仅本人:用户仅能查看自己创建或负责的数据,这有助于保护数据的私密性和责任归属。
  • 自定义:提供高度灵活性,允许根据"用户维度"或"部门维度"来定制数据访问规则,满足特定或复杂的业务需求。

实现原理

数据结构

在 v2.0.0 的数据权限模型中,核心关系围绕“用户 -> 系统角色 -> 菜单/按钮/数据范围”展开。它不再依赖独立的数据角色菜单,而是把数据范围作为系统角色在某个菜单上的一类授权配置。

表 / 结构作用
sys_user_role用户与系统角色的关系,一个用户可以拥有多个角色
sys_menu.use_data_scope菜单是否启用数据权限;未启用时,该菜单下的请求不会追加数据过滤条件
sys_role_menu角色与菜单/按钮/数据范围的统一关系表,permission_type=menu 表示功能授权,permission_type=scope 表示数据权限范围
sys_role_menu.data_scope_cd数据权限范围字典值,对应 data_scope:全部、本部门及以下、仅本部门、仅本人、自定义
sys_data_role_relation自定义数据权限的明细关系,保存角色 + 菜单 + 关联类型(用户/部门)+ 关联 ID
sys_user_dept / sys_dept_closure用户所属部门和部门闭包关系,用于计算本部门、本部门及以下范围
业务表 create_id / dept_scope数据归属字段,create_id 用于 user 模式,dept_scope 用于 dept 模式

这种设计的好处是:功能权限和数据权限在同一个角色授权弹窗中完成,菜单级开关决定哪些页面需要数据权限,Service 层再通过显式上下文决定哪些查询真正参与 SQL 注入,避免所有查询被默认拦截。

ER数据权限

UML时序

下述时序图详细展示了数据权限管理中的关键节点和操作流程。

UML数据权限

核心逻辑

用户登录与权限规则预处理

NOTE

dataScopeCd 类型如下:

字典值含义是否自定义
1006001全部
1006002本部门及以下
1006003仅本部门
1006004仅本人
1006005自定义

为了显著提升系统性能并缩短响应时间,我们在用户登录时引入了一项关键的预处理机制。系统将所有角色的数据权限配置聚合计算,结果存入 LoginUser Session,后续每次请求直接从 Session 读取,无需重复查库。

非自定义权限配置处理
  • 系统根据用户所拥有的全部角色,查询各角色在每个菜单上配置的 dataScopeCd(1006001~1006004)。
  • 同一菜单下若有多个角色配置,按"最宽松优先"规则合并:取所有 dataScopeCd 中编号最小的值作为最终结果(编号越小,权限范围越大)。
自定义权限配置处理
  • 对于 dataScopeCd=1006005 的自定义配置,系统从 sys_data_role_relation 表查询角色关联的部门 ID 集合与用户 ID 集合。
  • 多个角色的自定义范围取并集(UNION),合并为一个 CustomScope
合并规则细则

同一菜单下,非自定义规则与自定义规则可以同时存在。系统按以下四个分支处理:

情形处理方式
仅有非自定义规则(1006001~1006004)取所有角色中 dataScopeCd 最小值(最宽松),customScope 为空
仅有自定义规则(1006005)取各角色自定义范围的并集,以 customScope 存储
非自定义最小值为 1006001(全部)且同时存在自定义直接取 1006001 全部放行,自定义范围是其子集,被吸收,无需额外处理
非自定义最小值为 1006002~1006004 且同时存在自定义取非自定义最小值作为主规则,同时将自定义范围存入 extraCustomScope;SQL 执行时两个条件以 OR 拼接,取数据并集

组合场景举例

  • 角色A = 1006001(全部)+ 角色B = 1006005(自定义3个部门)→ 结果为全部
  • 角色A = 1006002(本部门及以下)+ 角色B = 1006005(自定义3个部门)→ 结果为本部门及以下的数据 OR 自定义3个部门的数据
  • 角色A = 1006004(仅本人)+ 角色B = 1006005(自定义3个部门)→ 结果为本人数据 OR 自定义3个部门的数据

TIP

自定义权限无论如何都会将当前登录用户自身 ID 加入 userIds,因此"仅本人 + 自定义"的组合本质上等价于"本人 OR 自定义指定的部门/用户"。

请求时多菜单命中规则

当一个 Controller 方法的 @SaCheckPermission 包含多个权限标识,且这些标识分属不同菜单时(OR 模式),系统遍历所有命中菜单的 dataScopeCd,取**编号最小(最宽松)**的那个菜单的权限配置作为本次请求的生效规则。

登录 Session 数据结构

LoginUser 是存储在 Sa-Token Session 中的权限载体,数据权限相关字段如下:

java
LoginUser {
    BaseUserInfo userInfo;              // 用户基础信息(id、username、nickname 等)
    Set<String>  permissions;           // 拥有的全部权限标识集合
    Set<String>  roles;                 // 拥有的全部角色编码集合
    List<Long>   depts;                 // 用户所属部门 ID 列表(直接所属)
    List<Long>   deptAndChildren;       // 用户所属部门及所有子孙部门 ID(用于"本部门及以下"规则)
    Map<String, Long>          permissionAndMenuIds; // 权限标识 → 菜单ID(Long 雪花ID)
    Map<Long,   RoleMenuScopeVO> dataScope;          // 菜单ID → 合并后的数据权限范围
}

RoleMenuScopeVO 结构(与 Redis 序列化字段顺序一致):

java
RoleMenuScopeVO {
    // 仅 dataScopeCd=1006005 时非空:纯自定义配置的部门/用户范围
    CustomScope customScope;

    String dataScopeCd;     // 生效的数据权限范围码(1006001~1006005)

    // 仅 dataScopeCd=1006002~1006004 且同时存在自定义配置时非空:附加的自定义 OR 条件
    CustomScope extraCustomScope;

    Long menuId;            // 菜单ID(雪花算法 Long)
}

CustomScope {
    Collection<Long> deptIds;   // 自定义可见部门 ID 集合
    Collection<Long> userIds;   // 自定义可见用户 ID 集合
}

NOTE

Redis 中 RoleMenuScopeVO 的字段序列化顺序为 customScope → dataScopeCd → extraCustomScope → menuId(Jackson 默认按字段声明顺序),与上方伪代码保持一致,方便直接对照 Redis 原始数据排查问题。

示例 JSON(登录后 Session 实际结构)

以下为 Redis 中实际存储的 LoginUser 数据(test4 账号,角色含"本部门及以下 + 自定义"组合场景)。字段顺序与 Redis 原始序列化完全一致,可直接对照排查。

json
{
  "dataScope": {
    "705327582698147908": {
      "customScope": null,
      "dataScopeCd": "1006002",
      "extraCustomScope": {
        "deptIds": [15],
        "userIds": [3, 5]
      },
      "menuId": 705327582698147908
    }
  },
  "deptAndChildren": [],
  "depts": [],
  "permissionAndMenuIds": {
    "teacher.statistics.update":      705327582698147908,
    "teacher.statistics.remove":      705327582698147908,
    "teacher.statistics.export":      705327582698147908,
    "teacher.statistics.query_table": 705327582698147908,
    "teacher.statistics.create":      705327582698147908,
    "teacher.statistics.import":      705327582698147908
  },
  "permissions": [
    "teacher.statistics.update",
    "teacher.statistics.remove",
    "teacher.statistics.export",
    "teacher.statistics.query_table",
    "teacher.statistics.create",
    "teacher.statistics.import"
  ],
  "roles": ["4", "7"],
  "userInfo": {
    "email": "",
    "id": 6,
    "logo": "",
    "nickname": "测试4-自定义",
    "phone": "",
    "username": "test4"
  }
}

NOTE

该示例对应**"非自定义(1006002 本部门及以下)+ 自定义"组合**:

  • dataScopeCd=1006002 为主规则(本部门及以下)
  • customScope=null:无纯自定义配置
  • extraCustomScope:附加的自定义 OR 条件(部门 15、用户 3 和 5)
  • SQL 执行时:本部门及以下 OR extraCustomScope 指定范围,取并集
  • test4 账号本身无部门(depts=[]),"本部门及以下"命中空集,最终生效数据仅来自 extraCustomScope

NOTE

菜单 ID 已从旧版 UUID 字符串改为雪花算法 Long 类型。旧版文档中的 UUID 格式(如 85b54322630f43a39296488a5e76ba16)已不再适用。

数据权限控制实现

系统采用 MybatisFlex 方言扩展机制,在 SQL 执行前自动拼接 WHERE 条件,对业务代码无侵入。

双方言架构

系统同时支持 MySQLPostgreSQL,通过抽象基类 AbstractPermissionDialect 统一流程,子类各自实现方言相关的 SQL 片段:

方言类适用数据库模块
MysqlPermissionDialectMySQL 8.0+sz-common-db-mysql
PostgresqlPermissionDialectPostgreSQLsz-common-db-postgresql

SQL 拼接规则

logic-min-unit=user(默认)模式为例,最终生效的 WHERE 条件结构如下:

非自定义规则(1006002~1006004)+ 有 extraCustomScope

sql
-- 本部门及以下(1006002)OR 自定义范围
(
  EXISTS (SELECT 1 FROM sys_user_dept WHERE dept_id IN (16,17,18) AND t.create_id = sys_user_dept.user_id)
  OR
  EXISTS (SELECT 1 FROM sys_user_dept WHERE dept_id IN (20,21) AND t.create_id = sys_user_dept.user_id)
  OR
  t.create_id IN (1234567890, 100)
)

纯自定义规则(1006005)

sql
-- 自定义部门范围 OR 自定义用户范围(含当前用户自身)
(
  EXISTS (SELECT 1 FROM sys_user_dept WHERE dept_id IN (30) AND t.create_id = sys_user_dept.user_id)
  OR
  t.create_id IN (1234567890, 200, 201)
)

全部(1006001):不追加任何 WHERE 条件,直接放行。

NOTE

allow-admin-view=false(默认)时,部门过滤条件会额外追加 AND NOT EXISTS (超管判断子查询),防止超管创建的数据通过部门条件被普通用户命中。allow-admin-view=true 则不追加此排除条件。


使用指南

下面我来详细描述下,如何配置和使用数据权限,在使用数据权限前,先进行以下的准备工作。

准备工作

在开始配置数据权限之前,请完成以下准备工作:

  • 确保 application.yml 配置文件中启用了数据权限功能:

    yaml
    sz:
      data-scope:
        enabled: true
        # 最小查询单位:user(按 create_id 字段)或 dept(按 dept_scope JSON 字段),默认 user
        logic-min-unit: user
        # 是否允许查看超管创建的数据,默认 false(超管数据不对普通用户可见)
        allow-admin-view: false
    配置项默认值说明
    enabledtrue数据权限总开关
    logic-min-unituseruser:以 create_id 字段过滤;dept:以 dept_scope JSON 字段过滤
    allow-admin-viewfalsefalse:超管创建的数据不会被普通用户的部门条件命中;true:超管数据对所有有部门权限的用户可见
  • 确认需要进行数据权限控制的表:

    • logic-min-unit=user 时:需要 create_id(Long 类型)字段
    • logic-min-unit=dept 时:需要 dept_scope(JSON 格式,List<Long>)字段
    • 两个字段由 EntityChangeListener 在数据插入时自动填充,无需手动维护

步骤一:新建菜单与设置权限

  1. 新建菜单项:在系统后台管理界面中,创建新的菜单项以代表不同的业务功能。

  2. 分配权限标识:为每个新建的菜单项分配一个唯一的 permission 标识符,该标识符将用于后续的权限控制逻辑。

  3. 开启数据权限支持:在菜单配置中找到并启用数据权限支持开关,确保该菜单项能够参与数据权限的控制流程。 scope-step1-1

    该开关会更新 sys_menu.use_data_scope。关闭时,角色授权弹窗中不会开放该菜单的数据权限配置;开启后,角色才能为该菜单选择具体数据范围。

  4. 使用注解标识权限:在与菜单项关联的Controller层方法上,应用 @SaCheckPermission 注解,并填入之前分配的 permission 标识符,以明确该方法的权限要求。

  5. 确保注解正确性:检查注解的语法和 permission 标识符的准确性,确保它们与菜单项的权限设置相匹配。

    scope-step1-2


步骤二:Service 层开启数据权限上下文

注意

SimpleDataScopeHelper 通过 TransmittableThreadLocal 存储每个线程的权限控制状态,以确保权限控制逻辑的隔离性和线程安全(支持线程池场景)。必须确保使用完毕后清理上下文,推荐使用以下两种写法之一。

推荐写法:DataScopeSession(try-with-resources,自动清理)

java
// DataScopeSession 实现 AutoCloseable,离开 try 块时自动调用 clearDataScope()
try (var ignored = new DataScopeSession(YourEntity.class)) {
    return PageUtils.getPageResult(queryChain().list());
}

备用写法:try-finally(手动清理)

java
try {
    SimpleDataScopeHelper.start(YourEntity.class); // 指定要追加权限条件的 PO 实体类
    return PageUtils.getPageResult(queryChain().list());
} finally {
    SimpleDataScopeHelper.clearDataScope(); // 必须在 finally 中清理,防止线程复用时状态污染
}

YourEntity.class 是需要被数据权限过滤的表对应的 PO 类。方言拦截器通过 @Table 注解反射获取表名,精确匹配目标查询。

例1: scope-step2-1.png 例2: sz-admin 数据权限步骤二示例二配置截图


步骤三:在系统角色中设置数据权限范围

  • 进入 系统管理 -> 角色管理,打开角色授权弹窗。
  • 左侧选择需要授权的菜单;只有 use_data_scope = T 的菜单会展示数据权限配置区域。
  • 为当前角色在当前菜单上选择数据权限范围。非自定义范围不需要额外选择部门,它依赖当前登录用户自身部门关系。
  • 自定义范围可以额外选择部门和用户,后端会写入 sys_data_role_relation

NOTE

角色提交时,前端会同时提交功能权限和 scope 数组;后端 SysRoleMenuServiceImpl.change 会先清理该角色旧的菜单/数据权限关系,再重新保存 sys_role_menusys_data_role_relation,最后发布权限变更事件。

非自定义Scope:

数据权限-非自定义 自定义Scope:

数据权限-自定义


步骤四:关联用户与系统角色

  • 将用户与相应的系统角色进行关联,确保用户能够根据角色获得菜单、按钮和数据范围。
  • 用户登录后,系统会根据其角色集合预计算 permissionAndMenuIdsdataScope,保存到 Sa-Token Session 中。
  • 后续请求命中 @SaCheckPermission 且对应菜单启用了数据权限时,方言会读取 Session 中的范围并拼接 SQL。

scope-step4-1.png


步骤五:测试验证

  • 在配置完成后,进行彻底的测试以验证数据权限的配置是否正确,确保权限控制按预期工作。
  • 至少验证列表查询、导出查询和涉及自定义部门/用户范围的组合场景。
  • 如果同一个用户拥有多个角色,建议单独验证“全部 + 自定义”“本部门及以下 + 自定义”“仅本人 + 自定义”等合并规则。

效果展示

为了直观展示数据权限控制的效果,我们以"教师统计"功能中的列表查询和Excel导出为例。以下是我们的演示设置:

账户与权限配置

我们准备了四个不同权限配置的账户,以展示不同数据权限对查询结果的影响:

账户部门数据权限设置
test1研发团队教师统计-本部门及以下部门
test2移动组教师统计-仅本部门
test3移动组教师统计-仅本人
test4无部门教师统计-自定义权限

权限效果验证

  • test1账户:具有查看"教师统计"数据的权限,包括其所属的"研发团队"及其所有子部门的数据。
  • test2账户:权限限制为仅查看其直接所属的"移动组"的数据。
  • test3账户:仅能查看其个人创建的"教师统计"数据。
  • test4账户:具有自定义权限,具体权限设置将根据实际业务需求进行配置。

操作步骤

  1. 登录账户:使用上述账户登录系统,密码默认为sz123456
  2. 访问教师统计列表:在系统中访问"教师统计"列表页面。
  3. 检查数据:观察并验证每个账户所看到的列表数据是否符合其数据权限设置的预期。

通过这一演示,您可以清晰地看到不同数据权限设置如何影响用户在系统中的数据访问结果。

预览图

user-test1

user-test2

user-test3

user-test4