基于Mybatis-Plus实现数据权限

概念

数据权限是指对系统用户进行数据资源可见性的控制。实现不同角色登录系统所展示的操作数据范围不一样,达到角色与角色、用户与用户之间数据的隔离。例如:管理员可以看到所有的菜单,而普通用户只能看到部分菜单。在同个表格数据中,管理员可以看到所有用户的数据,而普通用户只能查询到自己的数据。

1.引入依赖

<!-- SpringBoot工程 -->
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
    <version>版本自选</version>
</dependency>

2.基本使用

(1).数据权限枚举
import lombok.AllArgsConstructor;
import lombok.Getter;

/**
 * 数据权限枚举
 *
 * @author baymax
 * @date 2023-02-07 10:41
 */
@AllArgsConstructor
@Getter
public enum DataScope {

    ALL(1, "所有权限"),
    DEPARTMENT(2, "本部门"),
    SELF(3, "仅本人")
    ;

    private final Integer code;
    private final String description;

    public static DataScope findDataScope(Integer code) {
        for (DataScope value : DataScope.values()) {
            if (value.code.equals(code)) {
                return value;
            }
        }
        return null;
    }
}
(2).Mybatis-Plus配置类
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import com.baymax.interceptor.MybatisPlusPermissionInterceptor;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * mp配置类
 *
 * @author baymax
 * @date 2023-02-07 10:11
 */
@Configuration
public class MybatisPlusConfig {

    /**
     * 添加MP插件到Spring容器中
     */
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        // 分页插件
        interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.H2));
        // 数据权限插件,MybatisPlusPermissionInterceptor需自己实现
        interceptor.addInnerInterceptor(new MybatisPlusPermissionInterceptor());
        return interceptor;
    }
}
(3).Mybatis-Plus拦截器

注意:下面继承的JsqlParserSupport类,是Mybatis-Plus包里已经引入的,这是一个强大的SQL语句解析器,感兴趣的可以去GitHub上看看,地址:https://github.com/JSQLParser/JSqlParser

import com.baomidou.mybatisplus.core.toolkit.PluginUtils;
import com.baomidou.mybatisplus.extension.parser.JsqlParserSupport;
import com.baomidou.mybatisplus.extension.plugins.inner.InnerInterceptor;
import net.sf.jsqlparser.expression.Expression;
import net.sf.jsqlparser.statement.delete.Delete;
import net.sf.jsqlparser.statement.select.PlainSelect;
import net.sf.jsqlparser.statement.select.Select;
import net.sf.jsqlparser.statement.select.SelectBody;
import net.sf.jsqlparser.statement.select.SetOperationList;
import net.sf.jsqlparser.statement.update.Update;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;

import java.sql.Connection;
import java.sql.SQLException;
import java.util.List;

/**
 * Mybatis-Plus拦截器
 *
 * @author baymax
 * @date 2023-02-08 16:58
 */
public class MybatisPlusPermissionInterceptor extends JsqlParserSupport implements InnerInterceptor {

    /**
     * 数据权限解析器,拼接条件sql,需自己创建的类
     */
    private final MybatisPlusPermissionHandler mybatisPlusPermissionHandler = new MybatisPlusPermissionHandler();

    /**
     * 主要处理查询
     */
    @Override
    public void beforeQuery(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
        // 通过MP插件拿到即将执行的SQL
        PluginUtils.MPBoundSql mp = PluginUtils.mpBoundSql(boundSql);
        // parserSingle方法是JsqlParserSupport父类实现的方法,这里会根据执行的SQL是查询、新增、修改、删除来调用不同的方法,例如:如果是查询,就会调用当前类的processSelect方法
        mp.sql(parserSingle(mp.sql(), ms.getId()));
    }

    /**
     * 操作前置处理,可以在这里改改sql啥的
     */
    @Override
    public void beforePrepare(StatementHandler sh, Connection connection, Integer transactionTimeout) {
        InnerInterceptor.super.beforePrepare(sh, connection, transactionTimeout);
    }

    @Override
    protected void processDelete(Delete delete, int index, String sql, Object obj) {
        super.processDelete(delete, index, sql, obj);
    }

    @Override
    protected void processUpdate(Update update, int index, String sql, Object obj) {
        super.processUpdate(update, index, sql, obj);
    }

    @Override
    protected void processSelect(Select select, int index, String sql, Object obj) {
        SelectBody selectBody = select.getSelectBody();
        try {
            // 单个sql
            if (selectBody instanceof PlainSelect) {
                this.setWhere((PlainSelect) selectBody, obj.toString());
            } else if (selectBody instanceof SetOperationList) {
                // 多个sql,用;号隔开,一般不会用到。例如:select * from user;select * from role;
                SetOperationList setOperationList = (SetOperationList) selectBody;
                List<SelectBody> selects = setOperationList.getSelects();
                selects.forEach(s -> this.setWhere((PlainSelect) s, obj.toString()));
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    
    protected void setWhere(PlainSelect plainSelect, String mapperId) {
        Expression sqlSegment = mybatisPlusPermissionHandler.getSqlSegment(plainSelect.getWhere(), mapperId);
        if (null != sqlSegment) {
            plainSelect.setWhere(sqlSegment);
        }
    }
(4).Mybatis-Plus处理器

这里就是处理数据权限的主要类,来动态拼接sql的条件,达到数据隔离的效果

import com.baymax.enums.DataScope;
import com.baymax.utils.SecurityUserUtils;
import net.sf.jsqlparser.expression.Expression;
import net.sf.jsqlparser.expression.Parenthesis;
import net.sf.jsqlparser.expression.operators.conditional.AndExpression;
import net.sf.jsqlparser.parser.CCJSqlParserUtil;

/**
 * 数据权限处理器
 *
 * @author baymax
 * @date 2023-02-09 15:50
 */
public class MybatisPlusPermissionHandler {

    /**
     * 拼接数据权限sql的条件
     *
     * @param where sql语句的where条件
     * @param mapperId mapper执行的方法
     */
    public Expression getSqlSegment(Expression where, String mapperId) {
        //TODO 如果是管理员,拥有全部数据权限,不做任何条件语句的拼接,直接返回未处理的where
        if(管理员) {
            return where;
        }
        //TODO mapperId=类路径 + 方法。例如:com.xxx.UserMapper.selectList()
        // 结合自定义注解,通过反射的方式拿到方法上的注解。例如联查SQL,需要指定(别名.数据权限字段)拼接到where后面
        // if("判断mapperId执行方法上是否有注解") {
        //     "拿到value后,传入下面的buildDataFilter()方法里做条件拼接"
        // }
        
        // 构建查询条件
        String sql = buildDataFilter();
        if ("".equals(sql)) {
            return where;
        }
        try {
            Expression expression = CCJSqlParserUtil.parseExpression(sql);
            // 数据权限使用单独的括号 防止与其他条件冲突
            Parenthesis parenthesis = new Parenthesis(expression);
            if (null != where) {
                return new AndExpression(where, parenthesis);
            } else {
                return parenthesis;
            }
        } catch (Exception e) {
            throw new RuntimeException("数据权限解析异常 => " + e.getMessage());
        }
    }

    /**
     * 非常重要!!!
     * 拼接数据权限sql的条件,比如在我们登录成功时,会把用户、角色、数据权限信息缓存到容器上下文或者redis中,当非管理员用户访问某个接口时有查询一些数据,最后会到这个来,这时候我们可以从容器上下文或redis中拿到当前用户的信息,判断用户的数据权限,不同数据权限走不同的拼接SQL
     */
    private String buildDataFilter() {
        StringBuilder stringBuilder = new StringBuilder();
        DataScope dataScope = "拿当前用户的数据权限";
        // 部门数据权限
        if (DataScope.DEPARTMENT == dataScope) {
            // 例如:可以返回 "department_id IN (当前用户所属的部门id)"
            return "";
        } else if (DataScope.SELF == dataScope) {
            // 仅本人数据权限,create_by字段是表中的字段,这里不一定是这个字段,根据自己需求在需要做数据权限的表加上自定义得字段
            stringBuilder.append(" create_by").append("= '").append("拿当前用户的username").append("'");
            return stringBuilder.toString();
        }
        return "";
    }

注意:这里省略了登录的流程,这个自行实现

3.测试

当前登录的用户为:zzh

(1).即将查询的表数据

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2T6KsVOG-1676087324108)(../AppData/Roaming/Typora/typora-user-images/image-20230210152446624.png)]

(2).查询的结果

在这里插入图片描述

  • 6
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值