mldong 快速开发框架数据权限模块设计与实现

272 阅读7分钟

一、背景与需求分析

在企业级管理系统中,数据权限控制是保障系统安全、防止越权访问的重要手段之一。不同角色的用户对数据的访问范围应当受到限制,例如:

角色数据访问范围
超级管理员可访问所有数据
普通员工仅能查看本部门及子部门数据
部门主管可自定义数据权限
客户经理仅能看到自己创建的数据

传统的做法是在每个接口中手动拼接权限条件,这种方式不仅重复性高,还容易出错。为此,mldong 快速开发框架基于 MyBatis Plus 提供了统一的数据权限控制机制。


二、架构设计与实现原理

1. 整体流程图

2. 核心组件介绍

类名作用
@DataScope注解,用于标记需进行数据权限控制的方法或类
DataPermissionHandlerImpl实现 MyBatis Plus 的 DataPermissionHandler 接口,负责处理 SQL 拼接逻辑
DataAuthSqlBuilder接口,定义 SQL 构建器规范
DefaultDataAuthSqlBuilder默认实现类,根据角色权限动态构建 SQL
RoleDeptServiceImpl获取角色关联的部门 ID 列表
LoginUser登录用户信息,包含角色、部门、岗位等权限信息

三、MyBatis Plus 插件机制与 DataPermissionInterceptor 原理

✅ 1. MyBatis Plus 插件机制简介

MyBatis Plus 提供了一套插件机制(InnerInterceptor),允许开发者拦截 SQL 执行过程并修改其行为。

数据权限插件 DataPermissionInterceptor 就是通过 beforeQuery 拦截查询 SQL,并在其中动态拼接权限条件。

📌 功能说明:

  • 解析当前方法上的 @DataScope 注解。
  • 获取当前登录用户信息(LoginUser)。
  • 调用 SQL 构建器(DataAuthSqlBuilder)生成权限条件表达式。
  • 将权限表达式合并到原始 WHERE 条件中。

✅ 2. 插件配置示例

@Configuration
@MapperScan("com.mldong.**.mapper")
public class MybatisConfig {

    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        // 添加数据权限插件
        interceptor.addInnerInterceptor(new DataPermissionInterceptor(new DataPermissionHandlerImpl()));
        // 分页插件
        interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
        return interceptor;
    }
}

✅ 3. 插件定制aop

当使用DataPermissionInterceptor插件时,默认所有的select查询都会进行权限校验,会使用到<font style="color:#080808;background-color:#ffffff;">jsqlparser</font>工作去对sql进行校验,该校验过于严格,会让有些即使不需要进行数据权限的sql会报错,影响使用,所以这里使用DataPermissionAop切面的方式去判断是否开启数据权限。

主要使用<font style="color:#080808;background-color:#ffffff;">mybatis-plus</font><font style="color:#080808;background-color:#ffffff;">IgnoreStrategy</font>去控制。

package com.mldong.aop;

import com.baomidou.mybatisplus.core.plugins.IgnoreStrategy;
import com.baomidou.mybatisplus.core.plugins.InterceptorIgnoreHelper;
import com.mldong.annotation.DataScope;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;

/**
 * 控制数据权限插件的行为:
 * - 默认所有 Mapper 查询都不启用数据权限
 * - 加了 @DataScope 注解的方法/类,才启用数据权限逻辑
 */
@Aspect
@Component
public class DataPermissionAop {

    /**
     * 拦截所有 Mapper 方法
     */
    @Around("target(com.baomidou.mybatisplus.core.mapper.BaseMapper)")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        Method method = getMethodFrom(joinPoint);
        Class<?> targetClass = method.getDeclaringClass();

        // 获取类和方法上的 @DataScope 注解
        DataScope classAnnotation = targetClass.getAnnotation(DataScope.class);
        DataScope methodAnnotation = method.getAnnotation(DataScope.class);

        boolean isClassAnnotated = classAnnotation != null;
        boolean isMethodAnnotated = methodAnnotation != null;

        // 判断是否需要启用数据权限逻辑
        if (isClassAnnotated || isMethodAnnotated) {
            // 如果是方法单独设置 ignore=true,则忽略数据权限
            if (isMethodAnnotated && methodAnnotation.ignore()) {
                return handleWithIgnore(joinPoint); // 显式忽略
            }

            // 启用数据权限 → 不做任何 Ignore 处理,让 MyBatis Plus 插件生效
            return joinPoint.proceed();
        } else {
            // 没有任何注解 → 默认忽略数据权限
            return handleWithIgnore(joinPoint);
        }
    }

    /**
     * 带有 try-finally 的统一忽略策略处理
     */
    private Object handleWithIgnore(ProceedingJoinPoint joinPoint) throws Throwable {
        IgnoreStrategy strategy = IgnoreStrategy.builder().dataPermission(true).build();
        InterceptorIgnoreHelper.handle(strategy);
        try {
            return joinPoint.proceed();
        } finally {
            InterceptorIgnoreHelper.clearIgnoreStrategy();
        }
    }

    /**
     * 从 JoinPoint 中提取出当前执行的方法对象
     */
    private Method getMethodFrom(ProceedingJoinPoint joinPoint) {
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        return signature.getMethod();
    }
}


四、关键代码详解

1. 注解定义:@DataScope

@Inherited
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.TYPE})
public @interface DataScope {
    String deptFieldName() default "deptId";
    String tableAlias() default "";
    boolean ignore() default false;
    String userFieldName() default "createUser"; // Java 字段名 → create_user
    Class<? extends DataAuthSqlBuilder> clazz() default DefaultDataAuthSqlBuilder.class;
}

示例使用:

@Mapper
public interface UserMapper extends BaseMapper<User> {
    @DataScope(tableAlias = "t")
    List<UserVO> selectCustom(IPage<UserVO> page, @Param(Constants.WRAPPER) Wrapper<User> wrapper);
}

2. SQL 构建接口:DataAuthSqlBuilder

public interface DataAuthSqlBuilder {
    String build(LoginUser loginUser, DataScope dataScope);
}

3. 默认 SQL 构建器:DefaultDataAuthSqlBuilder

package com.mldong.auth.data.builder;

import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import com.mldong.annotation.DataScope;
import com.mldong.auth.LoginUser;
import com.mldong.auth.data.DataAuthSqlBuilder;
import com.mldong.auth.data.RoleDataScopeEnum;
import com.mldong.web.LoginUserHolder;
import org.springframework.stereotype.Component;

import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicBoolean;

@Component
public class DefaultDataAuthSqlBuilder implements DataAuthSqlBuilder {

    @Override
    public String build(LoginUser loginUser, DataScope dataScope) {
        String tableAlias = dataScope.tableAlias();
        String deptFieldName = StrUtil.toUnderlineCase(dataScope.deptFieldName());
        deptFieldName = StrUtil.isEmpty(tableAlias)?deptFieldName:tableAlias+"."+deptFieldName;
        String userFieldName = StrUtil.toUnderlineCase(dataScope.userFieldName());
        userFieldName = StrUtil.isEmpty(tableAlias)?userFieldName:tableAlias+"."+userFieldName;
        String sqlSegment = "";
        Map<Long,Integer> roleDataScopeMap = loginUser.getRoleDataScopeMap();
        if(ObjectUtil.isNotEmpty(roleDataScopeMap)
                && roleDataScopeMap.size()==1
                && roleDataScopeMap.containsValue(RoleDataScopeEnum.SELF_DATA.getCode())
        ){
            // 只能查看自己的数据
            sqlSegment = StrUtil.format("({} = {})", userFieldName, loginUser.getId());
            return sqlSegment;
        }
        StringBuilder sqlSegmentBuilder = new StringBuilder(sqlSegment);
        String finalDeptFieldName = deptFieldName;
        AtomicBoolean hasDeptDataScope = new AtomicBoolean(false);
        AtomicBoolean hasDeptAndSubDataScope = new AtomicBoolean(false);
        roleDataScopeMap.forEach((roleId, ds)->{
            if(ObjectUtil.equals(ds,RoleDataScopeEnum.CUSTOM_DATA.getCode())){
                // 自定义数据权限,会关联sys_role_dept表
                sqlSegmentBuilder.append(StrUtil.format(" {} in(select dept_id from sys_role_dept where role_id={}) or", finalDeptFieldName,roleId));
            } else if (ObjectUtil.equals(ds,RoleDataScopeEnum.DEPT_DATA.getCode())){
                // 本部门数据权限
                if(!hasDeptDataScope.get()){
                    sqlSegmentBuilder.append(StrUtil.format(" {} = {} or", finalDeptFieldName,loginUser.getDeptId()));
                    hasDeptDataScope.set(true);
                }
            } else if(ObjectUtil.equals(ds,RoleDataScopeEnum.DEPT_AND_SUB_DATA.getCode())){
                // 本部门及以下部门数据权限
                if(!hasDeptAndSubDataScope.get()){
                    List<Long> deptAllSubIdList = LoginUserHolder.getDeptAllSubIdList();
                    if(loginUser.getDeptId()!=null){
                        deptAllSubIdList.add(loginUser.getDeptId());
                    }
                    sqlSegmentBuilder.append(StrUtil.format(" {} in({}) or", finalDeptFieldName,StrUtil.join(",",deptAllSubIdList)));
                    hasDeptAndSubDataScope.set(true);
                }
            }
        });
        // 默认情况下,都可以查看自己的数据
        sqlSegmentBuilder.append(StrUtil.format(" {} = {} or", userFieldName, loginUser.getId()));
        if(sqlSegmentBuilder.length()>0){
            sqlSegment = sqlSegmentBuilder.substring(0,sqlSegmentBuilder.length()-3);
            sqlSegment = StrUtil.format("({})", sqlSegment);
        }
        return sqlSegment;
    }
}

✅ 字段命名转换逻辑说明:

  • dataScope.userFieldName() 默认为 "createUser"
  • 经过 StrUtil.toUnderlineCase(...) 后变为 "create_user"
  • 若设置了 tableAlias="t",则拼接成 "t.create_user"

符合团队字段命名规范


五、子部门权限的特殊处理:递归获取所有子部门 ID

✅ 场景说明:

当用户拥有“本部门及以下”权限时,需要获取当前部门及其所有子部门的 ID 列表,以便在 SQL 中使用 IN 查询。

✅ 递归获取子部门逻辑:

DeptServiceImpl.java

// file: DeptServiceImpl.java
@Override
    public List<Long> getAllSubDeptIds(Long parentId) {
        // 1. 从缓存中获取部门列表
        List<Dept> deptList = getDeptListInCache();
        // 这是子孙部门id集合
        List<Long> subDeptIds = new ArrayList<>();
        // 转换成树结构
        List<Dept> treeData = TreeTool.listToTree(deptList, parentId, Dept.class);
        // 递归获取子部门
        TreeTool.recursion(treeData, new IRecursionTree<Dept, List<Long>>() {
            @Override
            public void rowHandleBefore(Dept dept, List<Long> res) {
                res.add(dept.getId());
            }

            @Override
            public void rowHandleAfter(Dept dept, List<Long> res) {

            }
        }, subDeptIds);
        return subDeptIds;
    }

🧠 说明:

  • 使用递归方式从数据库中查询子部门;
  • 返回值 List<Long> 包含当前部门和所有子部门 ID;
  • DefaultDataAuthSqlBuilder 中被调用,用于构造 SQL 条件:
sqlSegmentBuilder.append(StrUtil.format(" {} in({}) or", deptFieldName, StrUtil.join(",", deptAllSubIdList)));

六、自定义权限(CUSTOM_DATA)使用示例

✅ 场景说明:

假设用户拥有多个角色:

  • 角色 A:数据权限类型为 CUSTOM_DATA,绑定部门 [100, 101]
  • 角色 B:数据权限类型为 DEPT_DATA,绑定部门 200
  • 角色 C:数据权限类型为 SELF_DATA,只看自己的数据

✅ 示例代码:

// 用户拥有的角色及其数据权限
loginUser.setRoleDataScopeMap(Map.of(
    1L, RoleDataScopeEnum.CUSTOM_DATA.getCode(),
    2L, RoleDataScopeEnum.DEPT_DATA.getCode(),
    3L, RoleDataScopeEnum.SELF_DATA.getCode()
));

✅ 最终生成的 SQL 片段:

(
    t.dept_id IN (SELECT dept_id FROM sys_role_dept WHERE role_id = 1)
    OR t.dept_id = 200
    OR t.create_user = 1000
)

七、多角色混合权限输出效果

✅ 场景 1:两个自定义权限

loginUser.setRoleDataScopeMap(Map.of(
    1L, RoleDataScopeEnum.CUSTOM_DATA.getCode(),   -- 部门 100, 101
    2L, RoleDataScopeEnum.CUSTOM_DATA.getCode()    -- 部门 102, 103
));

✅ SQL 输出:

(
    t.dept_id IN (SELECT dept_id FROM sys_role_dept WHERE role_id = 1)
    OR t.dept_id IN (SELECT dept_id FROM sys_role_dept WHERE role_id = 2)
    OR t.create_user = 1000
)

✅ 场景 2:自定义权限 + 子部门权限 + 自己的数据权限

loginUser.setRoleDataScopeMap(Map.of(
    1L, RoleDataScopeEnum.CUSTOM_DATA.getCode(),     -- 部门 100, 101
    2L, RoleDataScopeEnum.DEPT_AND_SUB_DATA.getCode() -- 当前部门 200 及子部门 201, 202
));

✅ SQL 输出:

(
    t.dept_id IN (SELECT dept_id FROM sys_role_dept WHERE role_id = 1)
    OR t.dept_id IN (200, 201, 202)
    OR t.create_user = 1000
)

八、测试用例与 SQL 输出效果汇总

场景描述SQL 条件
超级管理员无附加条件
仅自己数据权限(t.create_user = 1000)
本部门权限(ID: 100)(t.dept_id = 100 OR t.create_user = 1000)
本部门及子部门权限(ID: 100, 子部门: 101, 102)(t.dept_id IN (100, 101, 102) OR t.create_user = 1000)
自定义权限(角色 1 对应部门 100, 101)(t.dept_id IN (SELECT dept_id FROM sys_role_dept WHERE role_id=1) OR t.create_user = 1000)
多个自定义权限(角色 1 和 2)(t.dept_id IN (SELECT dept_id FROM sys_role_dept WHERE role_id=1) OR t.dept_id IN (SELECT dept_id FROM sys_role_dept WHERE role_id=2) OR t.create_user = 1000)
自定义 + 子部门(t.dept_id IN (SELECT dept_id FROM sys_role_dept WHERE role_id=1) OR t.dept_id IN (200, 201, 202) OR t.create_user = 1000)

九、总结与建议

✅ 优势:

  • 灵活扩展:支持多种权限类型组合。
  • 低侵入性:只需添加注解即可生效。
  • 性能优化:避免全表扫描,利用索引查询。
  • 字段命名规范createUsercreate_user,完全符合团队命名规范。
  • 组合权限支持:多个角色权限自动合并处理。

📌 建议:

  • 增加单元测试覆盖所有权限类型组合。
  • 支持字段级别权限控制。
  • 提供可视化界面配置角色数据权限
  • 异常日志记录与调试输出增强。

如需获取完整源码,请访问 Gitee 地址:

🔗 gitee.com/mldong/mldo…

十、附录:关键类对照表

类名路径职责
DataScopecom.mldong.annotation.DataScope注解定义
DataAuthSqlBuildercom.mldong.auth.data.DataAuthSqlBuilderSQL 构建接口
DefaultDataAuthSqlBuildercom.mldong.auth.data.builder.DefaultDataAuthSqlBuilder默认 SQL 构建实现
DataPermissionHandlerImplcom.mldong.mp.DataPermissionHandlerImpl权限处理器
DataPermissionAopcom.mldong.aop数据权限是否启用切面
MybatisConfigcom.mldong.config.MybatisConfig插件配置
UserMappercom.mldong.modules.sys.mapper.UserMapperMapper 层使用示例