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);

}
相关推荐
VX:Fegn08953 小时前
计算机毕业设计|基于springboot + vue电影院购票管理系统(源码+数据库+文档)
数据库·vue.js·spring boot·后端·课程设计
q_19132846953 小时前
基于SpringBoot2+Vue2的企业合作与活动管理平台
java·vue.js·经验分享·spring boot·笔记·mysql·计算机毕业设计
学网安的肆伍3 小时前
【040-安全开发篇】JavaEE应用&SpringBoot框架&JWT身份鉴权&打包部署JAR&WAR
spring boot·安全·java-ee
Han.miracle3 小时前
Spring WebMVC入门实战:从概念到连接建立全解析
java·spring boot·spring·springmvc
TT哇3 小时前
Spring Boot 项目中关于文件上传与访问的配置方案
java·spring boot·后端
韩立学长3 小时前
Springboot考研自习室预约管理系统1wdeuxh6(程序、源码、数据库、调试部署方案及开发环境)系统界面展示及获取方式置于文档末尾,可供参考。
数据库·spring boot·后端
残花月伴3 小时前
天机学堂-day5(互动问答)
java·spring boot·后端
北友舰长4 小时前
基于Springboot+thymeleaf图书管理系统的设计与实现【Java毕业设计·安装调试·代码讲解】
java·spring boot·mysql·课程设计·图书管理·b/s·图书
代码or搬砖12 小时前
RBAC(权限认证)小例子
java·数据库·spring boot