数据权限
在数据权限构建的这场复杂而深刻的旅程中,作者经过了深思熟虑和不懈的探索。我们在这里展示的解决方案,虽然可能不是完美无瑕,但它代表了我们对问题的一种理解和尝试。我们希望这不仅仅是一个方案的展示,更是一个思考的起点。
同时,作者在实现数据权限的过程中,不禁提出了一个关键问题:“我们是否真的需要构建如此复杂的数据权限体系?这样的复杂性是否真正为我们带来了价值?” 。 我们鼓励每一位用户,带着这样的问题,来体验此功能。
读前须知
注意事项
- 同步场景优先:当前方案主要针对
同步场景
设计,以确保数据一致性和实时性。我们认识到并非所有场景都适用,因此将持续探索以适应更多数据交互模式。 - 乐观权限控制:我们采取了一种乐观的权限管理策略,即不是所有数据都需要进行严格的权限控制。我们专注于对那些确实需要保护的敏感数据实施权限管理。
- 半自动化实现:为确保业务逻辑的清晰性和减少操作歧义,我们采用了半自动化的实现方式。这种方法结合了配置的灵活性和编码的精确性,以实现特定数据的权限控制。
- 最小权限单位:我们以菜单(menu)而非数据库表(table)作为数据权限控制的最小单位。这样做避免了用户需要进行大量表配置操作的繁琐,提高了效率和实用性。
- 注解依赖性:核心功能依赖于
@SaCheckPermission
注解来实现。为了确保数据权限的正常运行,请确保所有需要实现权限控制的Controller层都正确配置了该注解。这将帮助系统自动识别并应用相应的权限规则。 - 数据归属规则:我们的数据归属规则基于数据创建时的用户状态。这意味着,当用户创建数据时,系统将根据用户当时的部门归属来标记数据。例如,如果张三同时属于财务部和市场部,并且他创建了一条数据,系统将记录这条数据的创建者为
create_id="张三ID"
,并标注其部门范围为dept_scope="[财务部ID, 市场部ID]"
。这样的设计确保了数据归属的明确性,并有助于实现跨部门的数据管理和权限控制。 - 最小范围规则:系统默认对(已开启但未配置具体权限的)菜单项限制用户访问,仅允许访问本人数据,以保障数据安全。
内置规则
我们的系统内置了五种灵活的数据权限规则,以满足不同场景下的需求:
- 全部:用户被授予查看所有数据的权限,适用于需要全面数据访问的角色。
- 本部门及以下:用户可以查看自己所在部门及其所有子部门的数据。为实现这一规则,我们采用了闭包表结构(
sys_dept_closure表
),确保了查询的效率和速度。 - 仅本部门:用户只能访问自己所在部门的数据,适用于需要限制数据访问范围的情况。
- 仅本人:用户仅能查看自己创建或负责的数据,这有助于保护数据的私密性和责任归属。
- 自定义:提供高度灵活性,允许根据“用户维度”或“部门维度”来定制数据访问规则,满足特定或复杂的业务需求。
实现原理
数据结构
在我们的数据权限管理系统中,用户表、权限表、菜单表以及部门表之间建立了多对多的复杂关系。这种设计不仅增强了系统的灵活性,而且为不同的业务场景提供了广泛的支持。
- 用户与角色:用户可以拥有多个角色,而每个角色也可以被多个用户共享,从而实现了角色的灵活分配和权限的多样化管理。
- 角色与权限:每个角色可以关联多种权限,而同一种权限也可以被分配给不同的角色,确保了权限设置的细致和精确。
- 角色与菜单:角色与菜单之间的关系也是多对多的,允许角色访问多个菜单项,同时一个菜单项也可以被多个角色访问,提供了菜单访问的灵活性。
- 角色与部门:角色与部门之间的关系同样采用多对多模型,使得一个角色可以跨越不同部门,或者一个部门内可以有多个角色,增强了组织结构的适应性。
这种多对多的关系设计,不仅提高了系统的扩展性和可维护性,而且为实现复杂和动态的权限管理提供了坚实的基础。
UML时序
下述时序图详细展示了数据权限管理中的关键节点和操作流程。
核心逻辑
用户登录与权限规则预处理
NOTE
dataScopeCd类型如下:
字典值 | 含义 | 是否自定义 |
---|---|---|
1006001 | 全部 | 否 |
1006002 | 本部门及以下 | 否 |
1006003 | 仅本部门 | 否 |
1006004 | 仅本人 | 否 |
1006005 | 自定义 | 是 |
代码参考:
为了显著提升系统性能并缩短响应时间,我们在用户登录时引入了一项关键的预处理机制。这一机制专注于对数据权限规则(Rule)进行有效管理和优化,分为两大类:非自定义权限配置和自定义权限配置。
非自定义权限配置处理
- 系统将根据用户ID、用户角色所关联的菜单(menu)、所属部门等条件进行查询。
- 查询结果将经过二次聚合,生成一个RuleMap,为非自定义权限提供结构化的规则集。
自定义权限配置处理
- 类似地,系统根据用户ID、角色所关联的菜单(menu)、数据权限类型(dataScopeCd)等条件进行查询。
- 自定义权限是独立的存在,他与非自定义权限是OR的关系。
合并规则细则
- 最小权限最大规则:在相同菜单下,如果有多个部门的权限范围(scope),系统将选择最小值的scope作为最终权限(规则编号1006001至1006004)。
- 单一权限无需合并:对于只有一个scope的菜单,不需要进行额外的合并步骤。
- 自定义权限规则:对于具有自定义权限的用户,系统将分别获取其部门维度和用户维度的权限,做OR拼接。
RuleMap 权限规则映射
RuleMap 是一个关键的数据结构,用于存储和映射菜单(menu)与数据权限范围(dataScopeCd)之间的关系。这种映射以键值对(key-value)的形式存在,其中:
- Key (k):
menuId
,代表菜单的唯一标识符。 - Value (v):
dataScopeCd
,代表与该菜单关联的数据权限范围代码。
示例 JSON 格式的 RuleMap:
{
"ruleMap": {
"85b54322630f43a39296488a5e76ba16": "1006002"
}
}
在这个示例中,"85b54322630f43a39296488a5e76ba16"
是一个特定的 menuId
,而 "1006002"
是对应的数据权限范围代码。这表明对于具有该 menuId
的菜单项,用户的访问权限被限定为 dataScopeCd
所定义的范围。
RuleMap 的重要性:
- RuleMap 提供了一个快速查询的机制,允许系统迅速确定用户对于特定菜单项的访问权限。
- 它支持权限管理的灵活性和扩展性,便于在系统运行时动态调整权限设置。
- 通过预处理和缓存 RuleMap,系统能够减少权限验证的计算成本,从而提高响应速度。
通过这种方式,RuleMap 成为了我们权限管理系统中的一个高效组件,确保了权限控制的精确性和性能的优化。
预处理Session数据结构与示例
在我们的系统中,预处理Session用于存储用户登录后所需的关键数据,以便快速访问和权限验证。以下是Session中包含的主要数据结构及其描述:
- 所属部门及其子孙节点部门 (
deptAndChildren
):- 这是一个列表,包含用户所属部门的ID以及其所有子孙部门的ID。
- 权限标识与菜单关系数组 (
permissionAndMenuIds
):- 这是一个映射,将权限标识符映射到菜单ID,明确了不同权限对应的菜单项。
- 菜单的查询规则 (
ruleMap
):- 这是一个映射,将菜单ID映射到数据权限范围代码,定义了每个菜单项的访问规则。
- 自定义userRule(
userRuleMap
):- 这是一个映射,将菜单ID映射到userId集合,定义了可以访问指定菜单的指定用户集合。
- 自定义deptRule (
deptRuleMap
):- 这是一个映射,将菜单ID映射到dept集合,定义了可以访问指定菜单的指定部门集合。
示例JSON格式的预处理Session数据:
{
"depts": [
"java.util.ArrayList",
[
16
]
],
"deptAndChildren": [
"java.util.ArrayList",
[
16
]
],
"permissionAndMenuIds": {
"@class": "java.util.HashMap",
"teacher.statistics.update": "85b54322630f43a39296488a5e76ba16",
"teacher.statistics.remove": "85b54322630f43a39296488a5e76ba16",
"teacher.statistics.export": "85b54322630f43a39296488a5e76ba16",
"teacher.statistics.query_table": "85b54322630f43a39296488a5e76ba16",
"teacher.statistics.create": "85b54322630f43a39296488a5e76ba16",
"teacher.statistics.import": "85b54322630f43a39296488a5e76ba16"
},
"ruleMap": {
"@class": "java.util.HashMap",
"85b54322630f43a39296488a5e76ba16": "1006002"
},
"userRuleMap": {
"@class": "java.util.LinkedHashMap",
"85b54322630f43a39296488a5e76ba16": [
"java.util.HashSet",
[
3,
5
]
]
},
"deptRuleMap": {
"@class": "java.util.HashMap"
}
}
在这个示例中,我们展示了如何将部门信息、权限与菜单的映射关系、查询规则以及自定义的用户和部门ID存储在Session中。这种结构化的数据存储方式,使得在用户进行操作时,系统能够快速地进行权限验证和数据访问控制。
数据权限控制实现
NOTE
代码参考:
我们的数据权限控制流程基于Controller层的权限标识和RuleMap的结合来实现精确的数据访问控制:
- 权限标识与Menu关联:通过分析Controller上的权限
permission
标识,我们能够确定Controller与相应菜单项(menu)的关联关系,这是权限控制的第一步。 - RuleMap应用:结合RuleMap,我们可以进一步明确用户对特定菜单项的访问权限,为数据权限控制提供规则支持。
在实现数据权限控制时,我们采用了特定的SQL拼接技术来确保查询的权限正确性:
部门权限查询:通过嵌套的
EXISTS
查询,我们检查用户所属部门是否包含在指定的部门ID列表中,确保只有当用户与部门关联时,数据才可见。示例SQL如下所示:javaString sql = "EXISTS (SELECT 1 FROM `sys_user_dept` WHERE `sys_user_dept`.`dept_id` IN (部门ID列表) AND `" + alias + "`.`" + field + "` = `sys_user_dept`.`user_id`)";
超级管理员权限查询:对于具有超级管理员权限的用户,我们通过特定的字段检查来允许访问,示例SQL如下:
javaString sqlSuper = "EXISTS (SELECT 1 FROM `sys_user` WHERE `sys_user`.`id` = `" + alias + "`.`" + field + "` AND `sys_user`.`user_tag_cd` = '1001002' AND `del_flag` = 'F')";
个人权限查询:在处理个人权限时,我们使用了条件查询构造器来生成
=
语句,允许用户访问他们创建的数据。示例代码如下:java// sql = ' and create_id = 1 ' QueryCondition queryCondition = QueryCondition.create( new QueryColumn(table.getSchema(), table.getName(), field, table.getAlias()), SqlConsts.EQUALS, loginUser.getUserInfo().getId() );
自定义权限查询:我们将部门权限和个人权限结合起来,以实现更为复杂的自定义数据访问权限控制。
使用指南
下面我来详细描述下,如何配置和使用数据权限,在使用数据权限前,先进行以下的准备工作。
准备工作
在开始配置数据权限之前,请完成以下准备工作:
确保
application.yml
配置文件中启用了数据权限功能,设置配置参数:yamldata-scope: enable: true # 数据逻辑实现最小验证(查询)单位:dept 部门(dept_scope字段)、user 用户 (create_id 字段),默认user。 logic-min-unit: user
确认需要进行数据权限控制的表具备
create_id
(创建者ID,整型)和dept_scope
(部门范围,JSON格式)属性。(后续会修改属性结合自定义注解使用)
步骤一:新建菜单与设置权限
新建菜单项:在系统后台管理界面中,创建新的菜单项以代表不同的业务功能。
分配权限标识:为每个新建的菜单项分配一个唯一的
permission
标识符,该标识符将用于后续的权限控制逻辑。开启数据权限支持:在菜单配置中找到并启用数据权限支持开关,确保该菜单项能够参与数据权限的控制流程。
使用注解标识权限:在与菜单项关联的Controller层方法上,应用
@SaCheckPermission
注解,并填入之前分配的permission
标识符,以明确该方法的权限要求。确保注解正确性:检查注解的语法和
permission
标识符的准确性,确保它们与菜单项的权限设置相匹配。
步骤二:SQL层添加权限标识
!注意
SimpleDataScopeHelper
通过ThreadLocal
存储每个线程的权限控制状态,以确保权限控制逻辑的隔离性和线程安全。为了防止ThreadLocal
状态未被正确清理导致的潜在问题,我们强烈建议使用try-finally
代码块来严格管理权限控制逻辑:
初始化权限控制上下文: 在
try
块开始时,调用SimpleDataScopeHelper.start(YourEntity.class)
初始化当前线程的权限控制上下文。这为接下来的数据访问操作设定了权限控制的边界。javatry { SimpleDataScopeHelper.start(TeacherStatistics.class);
执行数据访问操作: 在
try
块中执行所有需要权限控制的数据访问业务逻辑。SimpleDataScopeHelper
将基于当前线程的上下文自动应用相应的权限控制。清理权限控制状态:
finally
块确保无论业务逻辑执行结果如何,都会调用SimpleDataScopeHelper.clearDataScope()
来清理ThreadLocal
状态。这一步是关键,它避免了ThreadLocal
中的残留状态影响到其他业务操作。java} finally { SimpleDataScopeHelper.clearDataScope(); }
遵循这一最佳实践不仅有助于保持业务逻辑的清晰性和准确性,而且确保了应用程序的线程安全性和稳定性。
在实现数据权限控制的核心环节中,SQL层的适当配置至关重要。请按照以下步骤在SQL查询中添加权限控制标识:
- 确定权限控制点:识别出需要根据用户权限过滤数据的SQL查询点。
- 使用权限控制辅助类:在相关的数据访问代码块中,使用
SimpleDataScopeHelper
类来管理权限控制的逻辑。 - 示例代码:
try {
SimpleDataScopeHelper.start(TeacherStatistics.class); // 指定要追加权限条件的数据表PO实体类,对此po类的关联表进行条件追加。
// 在此处编写或修改你的SQL查询,SimpleDataScopeHelper将自动根据权限规则追加条件
// 示例:SELECT * FROM teacher_statistics WHERE 1=1 你的查询条件
} finally {
SimpleDataScopeHelper.clearDataScope(); // 清除权限控制状态,避免影响其他操作
}
- 自定义SQL条件:如果需要,你可以在
SimpleDataScopeHelper.start()
和clearDataScope()
之间添加自定义的SQL条件,以满足特定的业务需求。 - 清理权限状态:在
finally
块中使用SimpleDataScopeHelper.clearDataScope()
确保在操作完成后清理权限控制状态,以防止对系统中的其他操作产生影响。
通过这些步骤,您可以确保数据权限的配置既符合业务逻辑,又满足安全合规要求,同时为用户提供了清晰的权限边界。 例1: 例2:
步骤三:设置数据权限角色
定义不同的数据权限角色,并为每个角色配置相应的权限规则。自定义权限外,不需单独配置部门,其依赖用户本身部门属性。
自定义权限可以任意配置部门和用户的规则。
非自定义Scope:
自定义Scope:
步骤四:关联用户与数据权限角色
- 将用户与相应的数据权限角色进行关联,确保用户能够根据其角色获得正确的数据访问权限。
步骤五:测试验证
- 在配置完成后,进行彻底的测试以验证数据权限的配置是否正确,确保权限控制按预期工作。
效果展示
为了直观展示数据权限控制的效果,我们以“教师统计”功能中的列表查询和Excel导出为例。以下是我们的演示设置:
账户与权限配置
我们准备了四个不同权限配置的账户,以展示不同数据权限对查询结果的影响:
账户 | 部门 | 数据权限设置 |
---|---|---|
test1 | 研发团队 | 教师统计-本部门及以下部门 |
test2 | 移动组 | 教师统计-仅本部门 |
test3 | 移动组 | 教师统计-仅本人 |
test4 | 无部门 | 教师统计-自定义权限 |
权限效果验证
- test1账户:具有查看“教师统计”数据的权限,包括其所属的“研发团队”及其所有子部门的数据。
- test2账户:权限限制为仅查看其直接所属的“移动组”的数据。
- test3账户:仅能查看其个人创建的“教师统计”数据。
- test4账户:具有自定义权限,具体权限设置将根据实际业务需求进行配置。
操作步骤
- 登录账户:使用上述账户登录系统,密码默认为
sz123456
。 - 访问教师统计列表:在系统中访问“教师统计”列表页面。
- 检查数据:观察并验证每个账户所看到的列表数据是否符合其数据权限设置的预期。
通过这一演示,您可以清晰地看到不同数据权限设置如何影响用户在系统中的数据访问结果。