数据权限
在数据权限构建的这场复杂而深刻的旅程中,作者经过了深思熟虑和不懈的探索。我们在这里展示的解决方案,虽然可能不是完美无瑕,但它代表了我们对问题的一种理解和尝试。我们希望这不仅仅是一个方案的展示,更是一个思考的起点。
同时,作者在实现数据权限的过程中,不禁提出了一个关键问题:"我们是否真的需要构建如此复杂的数据权限体系?这样的复杂性是否真正为我们带来了价值?" 。 我们鼓励每一位用户,带着这样的问题,来体验此功能。
读前须知
注意事项
- 同步场景优先:当前方案主要针对
同步场景设计,以确保数据一致性和实时性。我们认识到并非所有场景都适用,因此将持续探索以适应更多数据交互模式。 - 乐观权限控制:我们采取了一种乐观的权限管理策略,即不是所有数据都需要进行严格的权限控制。我们专注于对那些确实需要保护的敏感数据实施权限管理。
- 半自动化实现:为确保业务逻辑的清晰性和减少操作歧义,我们采用了半自动化的实现方式。这种方法结合了配置的灵活性和编码的精确性,以实现特定数据的权限控制。
- 最小权限单位:我们以菜单(menu)而非数据库表(table)作为数据权限控制的最小单位。这样做避免了用户需要进行大量表配置操作的繁琐,提高了效率和实用性。
- 注解依赖性:核心功能依赖于
@SaCheckPermission注解来实现。为了确保数据权限的正常运行,请确保所有需要实现权限控制的Controller层都正确配置了该注解。这将帮助系统自动识别并应用相应的权限规则。 - 数据归属规则:我们的数据归属规则基于数据创建时的用户状态。这意味着,当用户创建数据时,系统将根据用户当时的部门归属来标记数据。例如,如果张三同时属于财务部和市场部,并且他创建了一条数据,系统将记录这条数据的创建者为
create_id="张三ID",并标注其部门范围为dept_scope="[财务部ID, 市场部ID]"。这样的设计确保了数据归属的明确性,并有助于实现跨部门的数据管理和权限控制。 - 最小范围规则:系统默认对(已开启但未配置具体权限的)菜单项限制用户访问,仅允许访问本人数据,以保障数据安全。
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 注入,避免所有查询被默认拦截。
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 中的权限载体,数据权限相关字段如下:
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 序列化字段顺序一致):
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 原始序列化完全一致,可直接对照排查。
{
"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 条件,对业务代码无侵入。
双方言架构
系统同时支持 MySQL 和 PostgreSQL,通过抽象基类 AbstractPermissionDialect 统一流程,子类各自实现方言相关的 SQL 片段:
| 方言类 | 适用数据库 | 模块 |
|---|---|---|
MysqlPermissionDialect | MySQL 8.0+ | sz-common-db-mysql |
PostgresqlPermissionDialect | PostgreSQL | sz-common-db-postgresql |
SQL 拼接规则
以 logic-min-unit=user(默认)模式为例,最终生效的 WHERE 条件结构如下:
非自定义规则(1006002~1006004)+ 有 extraCustomScope:
-- 本部门及以下(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):
-- 自定义部门范围 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配置文件中启用了数据权限功能:yamlsz: 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_scopeJSON 字段过滤allow-admin-viewfalsefalse:超管创建的数据不会被普通用户的部门条件命中;true:超管数据对所有有部门权限的用户可见确认需要进行数据权限控制的表:
logic-min-unit=user时:需要create_id(Long 类型)字段logic-min-unit=dept时:需要dept_scope(JSON 格式,List<Long>)字段- 两个字段由
EntityChangeListener在数据插入时自动填充,无需手动维护
步骤一:新建菜单与设置权限
新建菜单项:在系统后台管理界面中,创建新的菜单项以代表不同的业务功能。
分配权限标识:为每个新建的菜单项分配一个唯一的
permission标识符,该标识符将用于后续的权限控制逻辑。开启数据权限支持:在菜单配置中找到并启用数据权限支持开关,确保该菜单项能够参与数据权限的控制流程。

该开关会更新
sys_menu.use_data_scope。关闭时,角色授权弹窗中不会开放该菜单的数据权限配置;开启后,角色才能为该菜单选择具体数据范围。使用注解标识权限:在与菜单项关联的Controller层方法上,应用
@SaCheckPermission注解,并填入之前分配的permission标识符,以明确该方法的权限要求。确保注解正确性:检查注解的语法和
permission标识符的准确性,确保它们与菜单项的权限设置相匹配。
步骤二:Service 层开启数据权限上下文
注意
SimpleDataScopeHelper 通过 TransmittableThreadLocal 存储每个线程的权限控制状态,以确保权限控制逻辑的隔离性和线程安全(支持线程池场景)。必须确保使用完毕后清理上下文,推荐使用以下两种写法之一。
推荐写法:DataScopeSession(try-with-resources,自动清理)
// DataScopeSession 实现 AutoCloseable,离开 try 块时自动调用 clearDataScope()
try (var ignored = new DataScopeSession(YourEntity.class)) {
return PageUtils.getPageResult(queryChain().list());
}备用写法:try-finally(手动清理)
try {
SimpleDataScopeHelper.start(YourEntity.class); // 指定要追加权限条件的 PO 实体类
return PageUtils.getPageResult(queryChain().list());
} finally {
SimpleDataScopeHelper.clearDataScope(); // 必须在 finally 中清理,防止线程复用时状态污染
}
YourEntity.class是需要被数据权限过滤的表对应的 PO 类。方言拦截器通过@Table注解反射获取表名,精确匹配目标查询。
例1:
例2: 
步骤三:在系统角色中设置数据权限范围
- 进入 系统管理 -> 角色管理,打开角色授权弹窗。
- 左侧选择需要授权的菜单;只有
use_data_scope = T的菜单会展示数据权限配置区域。 - 为当前角色在当前菜单上选择数据权限范围。非自定义范围不需要额外选择部门,它依赖当前登录用户自身部门关系。
- 自定义范围可以额外选择部门和用户,后端会写入
sys_data_role_relation。
NOTE
角色提交时,前端会同时提交功能权限和 scope 数组;后端 SysRoleMenuServiceImpl.change 会先清理该角色旧的菜单/数据权限关系,再重新保存 sys_role_menu 和 sys_data_role_relation,最后发布权限变更事件。
非自定义Scope:
自定义Scope:

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

步骤五:测试验证
- 在配置完成后,进行彻底的测试以验证数据权限的配置是否正确,确保权限控制按预期工作。
- 至少验证列表查询、导出查询和涉及自定义部门/用户范围的组合场景。
- 如果同一个用户拥有多个角色,建议单独验证“全部 + 自定义”“本部门及以下 + 自定义”“仅本人 + 自定义”等合并规则。
效果展示
为了直观展示数据权限控制的效果,我们以"教师统计"功能中的列表查询和Excel导出为例。以下是我们的演示设置:
账户与权限配置
我们准备了四个不同权限配置的账户,以展示不同数据权限对查询结果的影响:
| 账户 | 部门 | 数据权限设置 |
|---|---|---|
| test1 | 研发团队 | 教师统计-本部门及以下部门 |
| test2 | 移动组 | 教师统计-仅本部门 |
| test3 | 移动组 | 教师统计-仅本人 |
| test4 | 无部门 | 教师统计-自定义权限 |
权限效果验证
- test1账户:具有查看"教师统计"数据的权限,包括其所属的"研发团队"及其所有子部门的数据。
- test2账户:权限限制为仅查看其直接所属的"移动组"的数据。
- test3账户:仅能查看其个人创建的"教师统计"数据。
- test4账户:具有自定义权限,具体权限设置将根据实际业务需求进行配置。
操作步骤
- 登录账户:使用上述账户登录系统,密码默认为
sz123456。 - 访问教师统计列表:在系统中访问"教师统计"列表页面。
- 检查数据:观察并验证每个账户所看到的列表数据是否符合其数据权限设置的预期。
通过这一演示,您可以清晰地看到不同数据权限设置如何影响用户在系统中的数据访问结果。
预览图




