SpringBoot基于Mybatis拦截器实现数据权限(图文)

一、业务场景:

根据业务需要,这里将角色按照数据范围做权限限定,提供四级权限分别为:

1、全部:可以查看所有的数据;

2、部门及以下:按照部门架构,可以查看部门内所有人的数据;

3、个人:仅能查看由自己创建;

4、自定义:可以进行动态拼接,根据不同的字段拼接SQL。

二、思路:

1、定义Mybatis拦截器DataScopeInterceptor,用于每次拦截查询sql语句,附带数据范围权限的SQL条件;

2、定义注解DataScope及其它类,用来声明用于权限过滤的接口;

3、SpringBoot装配该拦截器。

注:这里如果有使用MybatisPlus的分页插件,需要保证执行顺序:DataScopeInterceptor > PaginationInterceptor

三、具体说明:

我们有单独存储权限的表结构,分为可读自己可操作自己、读部门及以下可操作所有、读所有及可操作所有、可操作所有读自定义、可操作自己读自定义。并且这个权限表是和菜单权限挂在一起的,根据每个角色配置菜单的同时可以配置当前菜单所拥有的数据权限,其实就是角色绑定菜单Id绑定权限表Id。然后页面通过查询菜单接口将每个菜单绑定的权限信息返回前端,前端在接口查询时带着对应的参数返回后端,后端根据不同的权限写不同的查询并拼接到执行的SQL中,固定读自己、读部门、读所有是按创建人字段进行in查询,自定义可以随意组合查询字段。

四、权限代码:

1、注解
java 复制代码
package com.sansint.canteen.datascope.annotation;

import com.sansint.canteen.datascope.constant.DatascopeConstants;
import com.sansint.canteen.datascope.enums.DataScopeType;
import com.sansint.canteen.datascope.holder.ExtraParamHolder;

import java.lang.annotation.*;

/**
 * 数据权限过滤注解
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DataScope {

    /**
     * 范围权限类别
     */
    public DataScopeType type() default DataScopeType.DATA_SCOPE_ALL;

    /**
     * 部门表的别名
     */
    public String deptAlias() default "d";

    /**
     * 用户表的别名
     */
    public String userAlias() default "u";

    /**
     * 是否附加"或条件"查询
     * 附加的"或条件sql片段",通过{@link ExtraParamHolder#setParam(String, Object)}设置,key为{@link DatascopeConstants#CUSTOM_OR_CONDITION}<br/>
     * 例:ExtraParamHolder.setParam(DatascopeConstants.CUSTOM_OR_CONDITION, " or cc_user_id = 5 ") 设置<br/>
     * 1. 不附加"或条件"查询后,查询语句为:
     * SELECT * FROM table WHERE condition1 = 1 and condition2 = 2 and (create_user IN (1,2,3))<br/>
     * 2. 附加"或条件"查询后,查询语句为:
     * SELECT * FROM table WHERE condition1 = 1 and condition2 = 2 and (create_user IN (1,2,3)  or cc_user_id = 5 )
     */
    boolean attachOrCondition() default false;
}
2、常量

这个根据实际项目中需要的字段进行调整

java 复制代码
package com.sansint.canteen.datascope.constant;

/**
 * 缓存的key 常量
 *
 * @author engineering
 */
public class DatascopeConstants
{
    /**
     * 数据权限code
     */
    public static final String READ_SCOPE_CODE = "readScopeCode";
    /**
     * 当前登录用户
     */
    public static final String SAN_SINT_USER = "sanSintUser";
    /**
     * 创建人字段名称
     */
    public static final String CREATE_USER = "create_user";
    /**
     * 自定义权限字段名称 ThreadLocal 中存储
     */
    public static final String CUSTOM_SCOPE = "read_custom";
    /**
     * 自定义或条件的sql片段
     */
    public static final String CUSTOM_OR_CONDITION = "customOrCondition";
}
3、枚举

这个根据实际项目中需要的字段进行调整

java 复制代码
package com.sansint.canteen.datascope.enums;

import java.util.Objects;

public enum DataScopeType {
    /**
     * 全部数据权限
     */
    DATA_SCOPE_ALL("read_all"),
    /**
     * 自定数据权限
     */
    DATA_SCOPE_CUSTOM("read_custom"),
    /**
     * 部门数据权限
     */
    DATA_SCOPE_DEPT("read_only_manage"),
    /**
     * 部门及以下数据权限
     */
    DATA_SCOPE_DEPT_AND_CHILD("read_manage"),
    /**
     * 仅本人数据权限
     */
    DATA_SCOPE_SELF("read_only");

    private String code;

    public String getCode() {
        return code;
    }

    DataScopeType(String code) {
        this.code = code;
    }

    public static DataScopeType of(String code) {

        Objects.requireNonNull(code, "数据范围权限类型不允许为空");

        for (DataScopeType dataScopeType : DataScopeType.values()) {
            if (dataScopeType.getCode().equals(code)) {
                return dataScopeType;
            }
        }

        throw new IllegalArgumentException(String.format("未识别的数据范围权限类型值[%s]", code));
    }


}
4、设置权限参数类

这个根据实际项目中需要的字段进行调整

java 复制代码
package com.sansint.canteen.datascope.holder;

import org.springframework.util.ObjectUtils;

import java.util.HashMap;
import java.util.Map;

import java.lang.ThreadLocal;

public class ExtraParamHolder {
    // 存储额外参数的 ThreadLocal,键为参数名,值为参数值
    private static final ThreadLocal<Map<String, Object>> extraParams = ThreadLocal.withInitial(HashMap::new);

    // 设置参数
    public static void setParam(String key, Object value) {
        if (ObjectUtils.isEmpty(key) || ObjectUtils.isEmpty(value)) {
            return;
        }
        extraParams.get().put(key, value);
    }

    // 获取参数
    public static Object getParam(String key) {
        return extraParams.get().get(key);
    }

    // 清除参数(避免内存泄漏)
    public static void clear() {
        extraParams.remove();
    }
}
5、拦截器
java 复制代码
package com.sansint.canteen.datascope.plugins;

import com.baomidou.mybatisplus.core.toolkit.PluginUtils;
import com.sansint.canteen.datascope.annotation.DataScope;
import com.sansint.canteen.datascope.constant.DatascopeConstants;
import com.sansint.canteen.datascope.enums.DataScopeType;
import com.sansint.canteen.datascope.holder.ExtraParamHolder;
import com.sansint.common.utils.SpringUtils;
import com.sansint.core.secure.SanSintUser;
import com.sansint.core.secure.utils.SecureUtil;
import com.sansint.system.user.feign.IUserClient;
import net.sf.jsqlparser.JSQLParserException;
import net.sf.jsqlparser.expression.Expression;
import net.sf.jsqlparser.expression.operators.conditional.AndExpression;
import net.sf.jsqlparser.parser.CCJSqlParserUtil;
import net.sf.jsqlparser.statement.Statement;
import net.sf.jsqlparser.statement.select.*;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.ParameterMapping;
import org.apache.ibatis.mapping.SqlCommandType;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.SystemMetaObject;
import org.apache.ibatis.session.Configuration;
import org.springframework.stereotype.Component;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;

import java.lang.reflect.Method;
import java.sql.Connection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.stream.Collectors;

import static com.sansint.canteen.datascope.constant.DatascopeConstants.CREATE_USER;
import static com.sansint.canteen.datascope.constant.DatascopeConstants.READ_SCOPE_CODE;

/**
 * 基于 JSQLParser 4.2 版本的数据权限拦截器
 */
@Component
@Intercepts({
    @Signature(
        type = StatementHandler.class,
        method = "prepare",
        args = {Connection.class, Integer.class}
    )
})
public class DataScopeInnerInterceptor implements Interceptor {

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        // 1. 获取 StatementHandler 及相关对象
        StatementHandler statementHandler = PluginUtils.realTarget(invocation.getTarget());
        MetaObject metaObject = SystemMetaObject.forObject(statementHandler);
        BoundSql boundSql = (BoundSql) metaObject.getValue("delegate.boundSql");
        MappedStatement mappedStatement = (MappedStatement) metaObject.getValue("delegate.mappedStatement");

        // 2. 只处理 SELECT 语句
        if (!SqlCommandType.SELECT.equals(mappedStatement.getSqlCommandType())) {
            return invocation.proceed();
        }

        // 3. 获取 Mapper 方法上的 @DataScope 注解
        DataScope dataScope = getDataSourceAnnotation(mappedStatement);
        if (dataScope == null) {
            return invocation.proceed(); // 无注解,直接执行
        }

        // 4、获取调用方法的参数
        Map<String, Object> paramValues = getParamValues(boundSql, mappedStatement);

        // 定义实际要使用的权限类型
        String actualType = null;
        if (paramValues.containsKey(READ_SCOPE_CODE)) {
            Object value = paramValues.get(READ_SCOPE_CODE);
            if (value != null) {
                actualType = value.toString();
            }
        }
        if (StringUtils.isEmpty(actualType)) {
            Object actualTypeObj = ExtraParamHolder.getParam(READ_SCOPE_CODE);
            if (actualTypeObj == null) {
                return invocation.proceed(); // 没有数据权限类型,直接执行
            }
            actualType = actualTypeObj.toString();
        }

        // 如果权限类型不存在,则无注解,直接执行
        if(StringUtils.isEmpty(actualType)){
            return invocation.proceed(); // 无注解,直接执行
        }

        // actualType 是 【read_only:operate_only】 这种格式的,只需要冒号前的部分
        actualType = actualType.split(":")[0];

        // 5. 解析原始 SQL,生成带权限条件的新 SQL
        String originalSql = boundSql.getSql();
        String newSql = buildDataScopeSql(originalSql, actualType, dataScope);
        if (StringUtils.hasText(newSql)) {
            metaObject.setValue("delegate.boundSql.sql", newSql); // 替换 SQL
        }

        return invocation.proceed();
    }

    /**
     * 获取 Mapper 方法上的 @DataScope 注解
     */
    private DataScope getDataSourceAnnotation(MappedStatement mappedStatement) throws ClassNotFoundException {
        String id = mappedStatement.getId(); // 格式:com.xxx.mapper.UserMapper.selectList
        String className = id.substring(0, id.lastIndexOf("."));
        String methodName = id.substring(id.lastIndexOf(".") + 1);
        methodName = methodName.replace("_COUNT", "");

        Class<?> mapperClass = Class.forName(className);
        for (Method method : mapperClass.getMethods()) {
            if (method.getName().equals(methodName) && method.isAnnotationPresent(DataScope.class)) {
                return method.getAnnotation(DataScope.class);
            }
        }
        return null;
    }

    // 解析参数对象,获取参数名-值映射(适配不同类型的参数,支持基本类型、Map、JavaBean 等)
    private Map<String, Object> getParamValues(BoundSql boundSql, MappedStatement mappedStatement) {
        // 获取参数名称列表(从 MappedStatement 中获取参数映射信息)
        List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();

        // 获取参数值(从 BoundSql 中获取实际参数对象)
        Object parameterObject = boundSql.getParameterObject();

        Configuration configuration = mappedStatement.getConfiguration();

        Map<String, Object> paramMap = new HashMap<>();
        if (parameterObject == null) {
            return paramMap;
        }

        // 1. 参数是 Map 类型(直接获取 key-value)
        if (parameterObject instanceof Map) {
            return (Map<String, Object>) parameterObject;
        }

        // 2. 参数是 JavaBean 或基本类型(通过 ParameterMapping 解析)
        MetaObject metaParam = configuration.newMetaObject(parameterObject);
        for (ParameterMapping mapping : parameterMappings) {
            String paramName = mapping.getProperty();
            Object value = metaParam.getValue(paramName);
            paramMap.put(paramName, value);
        }
        return paramMap;
    }

    /**
     * 构建带数据权限条件的 SQL(适配 JSQLParser 4.2)
     */
    private String buildDataScopeSql(String originalSql, String actualType, DataScope dataScope) throws JSQLParserException {
        // 解析 SQL 为抽象语法树(AST)
        Statement statement = CCJSqlParserUtil.parse(originalSql);
        Select select = (Select) statement;
        SelectBody selectBody = select.getSelectBody();

        // 处理不同类型的 SELECT 语句
        processSelectBody(selectBody, actualType, dataScope);

        // 将 AST 转回 SQL 字符串
        return select.toString();
    }

    /**
     * 处理 SELECT 主体(递归处理子查询)
     */
    private void processSelectBody(SelectBody selectBody, String actualType, DataScope dataScope) {
        if (selectBody instanceof PlainSelect) {
            // 处理简单查询(最常见场景)
            processPlainSelect((PlainSelect) selectBody, actualType, dataScope);
        } else if (selectBody instanceof WithItem) {
            WithItem withItem = (WithItem) selectBody;
            if (withItem.getSubSelect().getSelectBody() != null) {
                processSelectBody(withItem.getSubSelect().getSelectBody(), actualType, dataScope); // 递归处理子查询
            }
        } else if (selectBody instanceof SetOperationList) {
            // 处理联合查询(UNION/INTERSECT 等)
            SetOperationList setOperationList = (SetOperationList) selectBody;
            // JSQLParser 4.2 中 SetOperationList 的 getSelects() 返回 List<SelectBody>
            for (SelectBody body : setOperationList.getSelects()) {
                processSelectBody(body, actualType, dataScope);
            }
        }
    }

    /**
     * 处理简单查询(PlainSelect),添加数据权限条件
     */
    private void processPlainSelect(PlainSelect plainSelect, String actualType, DataScope dataScope) {
        // 1. 生成数据权限条件表达式
        Expression scopeCondition = buildScopeCondition(actualType, dataScope);
        if (scopeCondition == null) {
            return; // 无权限条件,不处理
        }

        // 2. 获取原始 WHERE 条件
        Expression where = plainSelect.getWhere();

        // 3. 拼接新的 WHERE 条件(原始条件 + 权限条件)
        if (where == null) {
            plainSelect.setWhere(scopeCondition); // 无原始条件,直接设为权限条件
        } else {
            plainSelect.setWhere(new AndExpression(where, scopeCondition)); // 有原始条件,用 AND 拼接
        }

//        // 4. 递归处理 FROM 子句中的子查询
//        FromItem fromItem = plainSelect.getFromItem();
//        if (fromItem instanceof SubSelect) {
//            SubSelect subSelect = (SubSelect) fromItem;
//            processSelectBody(subSelect.getSelectBody(), dataScope);
//        } else if (fromItem instanceof WithItem) {
//            WithItem withItem = (WithItem) fromItem;
//            processSelectBody(withItem.getSubSelect().getSelectBody(), dataScope);
//        }
//
//        // 5. 处理 JOIN 中的子查询(JSQLParser 4.2 中 Joins 为 List<Join>)
//        List<Join> joins = plainSelect.getJoins();
//        if (joins != null) {
//            for (Join join : joins) {
//                FromItem rightItem = join.getRightItem();
//                if (rightItem instanceof SubSelect) {
//                    SubSelect subSelect = (SubSelect) rightItem;
//                    processSelectBody(subSelect.getSelectBody(), dataScope);
//                }
//            }
//        }
    }

    /**
     * 构建数据权限条件表达式(核心逻辑)
     */
    private Expression buildScopeCondition(String actualType, DataScope dataScope) {
        try {
            // 实际场景:从 ThreadLocal 获取当前登录用户
            SanSintUser sanSintUser = SecureUtil.getUser();
            if (sanSintUser == null) {
                return null; // 未登录用户,无权限
            }

            // 获取注解中的表别名
            String deptAlias = dataScope.deptAlias(); // 部门表别名
            String userAlias = dataScope.userAlias(); // 用户表别名
            boolean attachOrCondition = dataScope.attachOrCondition(); // 是否附加"或条件"查询
            Object orCondition = null;
            if (attachOrCondition) {
                orCondition = ExtraParamHolder.getParam(DatascopeConstants.CUSTOM_OR_CONDITION);
            }

            // 生成权限条件(示例:部门权限 + 用户创建权限)
            StringBuilder condition = new StringBuilder();

            // 部门及以下数据权限
            if (actualType.equals(DataScopeType.DATA_SCOPE_DEPT_AND_CHILD.getCode())) {

                IUserClient userClient = SpringUtils.getBean(IUserClient.class);
                List<String> deptIdList = sanSintUser.getDeptIdList();
                //如果后期有我是A部门领导,B部门员工的话这地方需要调整
                List<Long> userIdList = userClient.selectUserIdListByDeptIds(deptIdList);
                String userIdStr = "0";
                if (userIdList != null && !userIdList.isEmpty()) {
                    userIdStr = userIdList.stream().map(String::valueOf).collect(Collectors.joining(","));
                }

                condition
                    .append("(")// 给or条件预添加括号,即使没有or条件,括号也不影响执行结果
                    .append(StringUtils.isEmpty(userAlias) ? CREATE_USER + " in (" : userAlias + "." + CREATE_USER + " in (")
                    //.append(StringUtils.isEmpty(deptAlias) ? userAlias + "." + CREATE_USER + " in (" : deptAlias + "." + CREATE_USER + " in (")
                    .append(userIdStr)
                    .append(") ")
                    .append(attachOrCondition && !ObjectUtils.isEmpty(orCondition) ? orCondition : "")
                    .append(")");
            }

            // 仅本人数据权限:create_by = 当前用户ID
            if (actualType.equals(DataScopeType.DATA_SCOPE_SELF.getCode())) {
                if (condition.length() > 0) {
                    condition.append(" AND "); // 拼接多个条件
                }
                condition
                    .append("(")// 给or条件预添加括号,即使没有or条件,括号也不影响执行结果
                    .append(StringUtils.isEmpty(userAlias) ? CREATE_USER + " = " : userAlias + "." + CREATE_USER + " = ")
                    .append(sanSintUser.getUserId().toString())
                    .append(" ")
                    .append(attachOrCondition && !ObjectUtils.isEmpty(orCondition) ? orCondition : "")
                    .append(")");
            }
            // 自定义数据权限 :自定义SQL语句
            if (actualType.equals(DataScopeType.DATA_SCOPE_CUSTOM.getCode())) {
                if (condition.length() > 0) {
                    condition.append(" AND "); // 拼接多个条件
                }
                Object param = ExtraParamHolder.getParam(DatascopeConstants.CUSTOM_SCOPE);
                condition
                    .append("(")
                    .append(StringUtils.isEmpty(userAlias) ? param : userAlias + "." + param)
                    .append(")");
            }

            if (condition.length() == 0) {
                return null; // 无权限条件
            }

            // JSQLParser 4.2 中解析条件表达式的方法
            return CCJSqlParserUtil.parseCondExpression(condition.toString());
        } catch (JSQLParserException e) {
            throw new RuntimeException("生成数据权限条件失败", e);
        }
    }

    @Override
    public Object plugin(Object target) {
        if (target instanceof StatementHandler) {
            return Plugin.wrap(target, this); // 只代理 StatementHandler
        }
        return target;
    }

    @Override
    public void setProperties(Properties properties) {
        // 可配置参数(如默认表别名)
    }

}
6、入口业务接口

根据前端传的权限字段及值,存入ThreadLocal中且使用后销毁。

java 复制代码
/**
     * 条件查询列表数据
     *
     * @param canteenAttendanceDto 查询条件
     */
    @Override
    public List<CanteenAttendance> list(CanteenAttendanceDto canteenAttendanceDto, SanSintUser sanSintUser) {
        List<CanteenAttendance> canteenAttendanceList;
        try {
            ExtraParamHolder.setParam(READ_SCOPE_CODE, canteenAttendanceDto.getReadScopeCode());
            canteenAttendanceList = canteenAttendanceMapper.selectList(new LambdaQueryWrapper<CanteenAttendance>()
                .eq(CanteenAttendance::getTenantId, sanSintUser.getTenantId())
                // 逻辑删除字段
                .eq(CanteenAttendance::getDeletedFlag, NumberConstant.ZERO)
                .eq(Objects.nonNull(canteenAttendanceDto.getIsEstablish()), CanteenAttendance::getIsEstablish, canteenAttendanceDto.getIsEstablish())
                .eq(Objects.nonNull(canteenAttendanceDto.getDeptId()), CanteenAttendance::getDeptId, canteenAttendanceDto.getDeptId())
                .eq(Objects.nonNull(canteenAttendanceDto.getPersonId()), CanteenAttendance::getPersonId, canteenAttendanceDto.getPersonId())
                .like(Objects.nonNull(canteenAttendanceDto.getPersonNum()), CanteenAttendance::getPersonNum, canteenAttendanceDto.getPersonNum())
                .eq(Objects.nonNull(canteenAttendanceDto.getMonth()), CanteenAttendance::getMonth, canteenAttendanceDto.getMonth())
                .eq(Objects.nonNull(canteenAttendanceDto.getYear()), CanteenAttendance::getYear, canteenAttendanceDto.getYear())
                .orderByDesc(CanteenAttendance::getCreateTime));
        } finally {
            ExtraParamHolder.clear();
        }
        return canteenAttendanceList;
    }
7、入口Mapper接口

这个注解是必须加的,一般使用最多就是第二个参数,如果涉及多表联查的话,把主表别名写在第二个参数上。如果单表则图中所示即可。

java 复制代码
package com.sansint.canteen.mapper.administration;

import com.baomidou.mybatisplus.annotation.SqlParser;
import com.baomidou.mybatisplus.core.conditions.Wrapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.sansint.canteen.datascope.annotation.DataScope;
import com.sansint.canteen.domain.administration.entity.CanteenAttendance;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;

import java.util.List;


@Mapper
public interface CanteenAttendanceMapper extends BaseMapper<CanteenAttendance> {
    /**
     * 查询所有行
     * @param queryWrapper 查询条件
     * @return 对象列表
     */
    @DataScope(deptAlias = "", userAlias = "")
    List<CanteenAttendance> selectList(@Param("ew") Wrapper<CanteenAttendance> queryWrapper);

}
相关推荐
_UMR_18 分钟前
springboot集成Jasypt实现配置文件启动时自动解密-ENC
java·spring boot·后端
蓝色王者1 小时前
springboot 2.6.13 整合flowable6.8.1
java·spring boot·后端
hashiqimiya3 小时前
springboot事务触发滚动与不滚蛋
java·spring boot·后端
因我你好久不见3 小时前
Windows部署springboot jar支持开机自启动
windows·spring boot·jar
无关86884 小时前
SpringBootApplication注解大解密
spring boot
追梦者1236 小时前
springboot整合minio
java·spring boot·后端
帅气的你6 小时前
Spring Boot 集成 AOP 实现日志记录与接口权限校验
java·spring boot
计算机毕设VX:Fegn08957 小时前
计算机毕业设计|基于springboot + vue在线音乐播放系统(源码+数据库+文档)
数据库·vue.js·spring boot·后端·课程设计
计算机毕设VX:Fegn08957 小时前
计算机毕业设计|基于springboot + vue博物馆展览与服务一体化系统(源码+数据库+文档)
数据库·vue.js·spring boot·后端·课程设计
帅气的你7 小时前
Spring Boot 1.x 接口性能优化:从 3 秒到 200 毫秒的实战调优之路
java·spring boot