一、背景与目标
在若依框架原有 DataScope 的基础上,实现一套独立的、基于部门层级的数据权限过滤机制,用于按组织结构灵活控制数据可见范围。
设计目标
-
不依赖角色、不判断是否管理员
-
通过注解参数动态控制数据范围
-
支持:
- 是否包含本部门
- 向上查询 N 级部门
- 向下查询 N 级部门 / 所有子部门
-
与若依原有
BaseEntity + params + MyBatis XML机制完全兼容
二、核心设计思路
1. 技术方案
-
使用 AOP + 自定义注解 拦截查询方法
-
在方法执行前:
- 根据当前用户部门 ID
- 动态拼接部门过滤 SQL
- 注入到
BaseEntity.params.dataScope
-
Mapper XML 中通过
${params.dataScope}拼接 WHERE 条件
2. 依赖表结构(sys_dept)
text
dept_id 部门ID
parent_id 父部门ID
ancestors 祖先路径,如:0,1,3,10
三、自定义注解:ExtendedDataScope
java
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ExtendedDataScope {
// 部门表别名(必填)
String deptAlias() default "";
// 用户表别名(预留扩展)
String userAlias() default "";
// 权限类型(当前主要使用 dept)
String type() default "dept";
// 向上级部门层数(0 = 不包含)
int upLevel() default 0;
// 向下级部门层数(0 = 不包含,999 = 所有子级)
int downLevel() default 0;
// 是否包含本部门
boolean includeSelf() default true;
}
四、AOP 实现要点
1. 切面职责
- 拦截所有标注
@ExtendedDataScope的方法 - 清空历史 dataScope,防止 SQL 注入
- 生成部门层级 SQL
- 写入
BaseEntity.params.dataScope
2. 核心处理流程
text
@Before
├─ clearDataScope()
├─ 获取当前用户
├─ 读取注解参数
├─ 构建部门范围 SQL
└─ 写入 params.dataScope
点击查看代码
@Aspect
@Component
public class ExtendedDataScopeAspect {
/**
* 参数 key(和若依一致)
*/
public static final String DATA_SCOPE = "dataScope";
@Before("@annotation(dataScope)")
public void doBefore(JoinPoint joinPoint, ExtendedDataScope dataScope) {
clearDataScope(joinPoint);
handleDataScope(joinPoint, dataScope);
}
private void handleDataScope(JoinPoint joinPoint, ExtendedDataScope dataScope) {
LoginUser loginUser = SecurityUtils.getLoginUser();
if (loginUser == null) {
return;
}
String deptAlias = dataScope.deptAlias();
if (StringUtils.isBlank(deptAlias)) {
return;
}
String sql = buildDeptScopeSql(loginUser.getDeptId(), deptAlias, dataScope);
if (StringUtils.isBlank(sql)) {
return;
}
Object params = joinPoint.getArgs()[0];
if (params instanceof BaseEntity) {
BaseEntity baseEntity = (BaseEntity) params;
baseEntity.getParams().put(DATA_SCOPE, " AND (" + sql + ")");
}
}
/**
* 构建部门层级 SQL
*/
private String buildDeptScopeSql(Long deptId, String deptAlias, ExtendedDataScope scope) {
List<String> conditions = new ArrayList<>();
/* ========== 本部门 ========== */
if (scope.includeSelf()) {
conditions.add(deptAlias + ".dept_id = " + deptId);
}
/* ========== 向上 ========== */
if (scope.upLevel() > 0) {
// ancestors 形如:0,1,3,10
// 向上 N 级:取 ancestors 中倒数 N 位
conditions.add(buildUpDeptSql(deptAlias, deptId, scope.upLevel()));
}
/* ========== 向下 ========== */
if (scope.downLevel() > 0) {
if (scope.downLevel() >= 999) {
// 所有子级
conditions.add(
deptAlias + ".dept_id IN (" +
"SELECT dept_id FROM sys_dept " +
"WHERE find_in_set(" + deptId + ", ancestors)" +
")"
);
} else {
conditions.add(buildDownDeptSql(deptAlias, deptId, scope.downLevel()));
}
}
return String.join(" OR ", conditions);
}
/**
* 向上 N 级部门
*/
private String buildUpDeptSql(String deptAlias, Long deptId, int upLevel) {
// 使用子查询,取 ancestors 中的上级
return deptAlias + ".dept_id IN (" +
" SELECT t.dept_id FROM sys_dept t " +
" WHERE t.dept_id IN ( " +
" SELECT SUBSTRING_INDEX(" +
" SUBSTRING_INDEX(d.ancestors, ',', -( " + upLevel + " + 1)), ',', 1" +
" ) FROM sys_dept d WHERE d.dept_id = " + deptId +
" )" +
")";
}
/**
* 向下 N 级部门
*/
private String buildDownDeptSql(String deptAlias, Long deptId, int downLevel) {
// ancestors 深度控制(当前 depth + N)
return deptAlias + ".dept_id IN (" +
" SELECT d.dept_id FROM sys_dept d " +
" WHERE find_in_set(" + deptId + ", d.ancestors) " +
" AND (LENGTH(d.ancestors) - LENGTH(REPLACE(d.ancestors, ',', ''))) <= " +
" ( " +
" SELECT (LENGTH(ancestors) - LENGTH(REPLACE(ancestors, ',', ''))) + " + downLevel +
" FROM sys_dept WHERE dept_id = " + deptId +
" )" +
")";
}
/**
* 清空 dataScope,防止 SQL 注入
*/
private void clearDataScope(JoinPoint joinPoint) {
Object params = joinPoint.getArgs()[0];
if (params instanceof BaseEntity) {
((BaseEntity) params).getParams().put(DATA_SCOPE, "");
}
}
}
五、部门层级 SQL 构建规则
1. 本部门
sql
d.dept_id = {currentDeptId}
由 includeSelf = true 控制
2. 向上 N 级部门(upLevel)
原理:
- 利用
ancestors字段 - 从 ancestors 中向前截取 N 个父级
示意 SQL:
sql
d.dept_id IN (
SELECT SUBSTRING_INDEX(
SUBSTRING_INDEX(ancestors, ',', -(N + 1)), ',', 1
)
FROM sys_dept
WHERE dept_id = 当前部门ID
)
3. 向下 N 级部门(downLevel)
3.1 所有子级(downLevel = 999)
sql
d.dept_id IN (
SELECT dept_id FROM sys_dept
WHERE find_in_set(当前部门ID, ancestors)
)
3.2 限定层级子部门
思路:
- 计算 ancestors 的层级深度(逗号个数)
- 控制最大深度 = 当前深度 + N
sql
d.dept_id IN (
SELECT d.dept_id
FROM sys_dept d
WHERE find_in_set(当前部门ID, d.ancestors)
AND 层级深度(d) <= 当前层级 + N
)
六、Mapper XML 使用方式
xml
<select id="selectList" resultType="xxx">
SELECT *
FROM biz_table t
LEFT JOIN sys_dept d ON t.dept_id = d.dept_id
<where>
1 = 1
${params.dataScope}
</where>
</select>
说明:
${params.dataScope}必须保留- AOP 动态注入
AND ( ... )
七、使用示例
java
@ExtendedDataScope(
deptAlias = "d",
includeSelf = true,
upLevel = 1,
downLevel = 2
)
public List<SysDept> selectDeptList(SysDept dept)
{
return deptMapper.selectDeptList(dept);
}
含义说明
| 参数 | 含义 |
|---|---|
| includeSelf | 包含本部门 |
| upLevel=1 | 包含上一级部门 |
| downLevel=2 | 包含下两级部门 |
| downLevel=999 | 包含所有子部门 |
八、方案特点总结
- ✅ 与若依原生 DataScope 解耦
- ✅ 仅依赖部门层级,不依赖角色权限
- ✅ 控制粒度细,适合复杂组织结构
- ✅ 非侵入式,Mapper 无需改动
- ✅ 特别适合安全监管 / GIS / 组织树场景
九、可扩展方向(后续优化)
- 使用 MySQL 8 / PostgreSQL 的
WITH RECURSIVE优化层级查询 - 部门层级缓存(Redis)减少子查询
- 扩展到:部门 + 用户混合数据权限
- 支持多部门归属(兼职部门)