概念
数据权限是指对系统用户进行数据资源可见性的控制。实现不同角色登录系统所展示的操作数据范围不一样,达到角色与角色、用户与用户之间数据的隔离。例如:管理员可以看到所有的菜单,而普通用户只能看到部分菜单。在同个表格数据中,管理员可以看到所有用户的数据,而普通用户只能查询到自己的数据。
1.引入依赖
<!-- SpringBoot工程 --><dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-boot-starter</artifactId><version>版本自选</version></dependency>
2.基本使用
(1).数据权限枚举
importlombok.AllArgsConstructor;importlombok.Getter;/**
* 数据权限枚举
*
* @author baymax
* @date 2023-02-07 10:41
*/@AllArgsConstructor@GetterpublicenumDataScope{ALL(1,"所有权限"),DEPARTMENT(2,"本部门"),SELF(3,"仅本人");privatefinalInteger code;privatefinalString description;publicstaticDataScopefindDataScope(Integer code){for(DataScope value :DataScope.values()){if(value.code.equals(code)){return value;}}returnnull;}}
(2).Mybatis-Plus配置类
importcom.baomidou.mybatisplus.annotation.DbType;importcom.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;importcom.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;importcom.baymax.interceptor.MybatisPlusPermissionInterceptor;importorg.mybatis.spring.annotation.MapperScan;importorg.springframework.context.annotation.Bean;importorg.springframework.context.annotation.Configuration;/**
* mp配置类
*
* @author baymax
* @date 2023-02-07 10:11
*/@ConfigurationpublicclassMybatisPlusConfig{/**
* 添加MP插件到Spring容器中
*/@BeanpublicMybatisPlusInterceptormybatisPlusInterceptor(){MybatisPlusInterceptor interceptor =newMybatisPlusInterceptor();// 分页插件
interceptor.addInnerInterceptor(newPaginationInnerInterceptor(DbType.H2));// 数据权限插件,MybatisPlusPermissionInterceptor需自己实现
interceptor.addInnerInterceptor(newMybatisPlusPermissionInterceptor());return interceptor;}}
(3).Mybatis-Plus拦截器
注意:下面继承的JsqlParserSupport类,是Mybatis-Plus包里已经引入的,这是一个强大的SQL语句解析器,感兴趣的可以去GitHub上看看,地址:https://github.com/JSQLParser/JSqlParser
importcom.baomidou.mybatisplus.core.toolkit.PluginUtils;importcom.baomidou.mybatisplus.extension.parser.JsqlParserSupport;importcom.baomidou.mybatisplus.extension.plugins.inner.InnerInterceptor;importnet.sf.jsqlparser.expression.Expression;importnet.sf.jsqlparser.statement.delete.Delete;importnet.sf.jsqlparser.statement.select.PlainSelect;importnet.sf.jsqlparser.statement.select.Select;importnet.sf.jsqlparser.statement.select.SelectBody;importnet.sf.jsqlparser.statement.select.SetOperationList;importnet.sf.jsqlparser.statement.update.Update;importorg.apache.ibatis.executor.Executor;importorg.apache.ibatis.executor.statement.StatementHandler;importorg.apache.ibatis.mapping.BoundSql;importorg.apache.ibatis.mapping.MappedStatement;importorg.apache.ibatis.session.ResultHandler;importorg.apache.ibatis.session.RowBounds;importjava.sql.Connection;importjava.sql.SQLException;importjava.util.List;/**
* Mybatis-Plus拦截器
*
* @author baymax
* @date 2023-02-08 16:58
*/publicclassMybatisPlusPermissionInterceptorextendsJsqlParserSupportimplementsInnerInterceptor{/**
* 数据权限解析器,拼接条件sql,需自己创建的类
*/privatefinalMybatisPlusPermissionHandler mybatisPlusPermissionHandler =newMybatisPlusPermissionHandler();/**
* 主要处理查询
*/@OverridepublicvoidbeforeQuery(Executor executor,MappedStatement ms,Object parameter,RowBounds rowBounds,ResultHandler resultHandler,BoundSql boundSql)throwsSQLException{// 通过MP插件拿到即将执行的SQLPluginUtils.MPBoundSql mp =PluginUtils.mpBoundSql(boundSql);// parserSingle方法是JsqlParserSupport父类实现的方法,这里会根据执行的SQL是查询、新增、修改、删除来调用不同的方法,例如:如果是查询,就会调用当前类的processSelect方法
mp.sql(parserSingle(mp.sql(), ms.getId()));}/**
* 操作前置处理,可以在这里改改sql啥的
*/@OverridepublicvoidbeforePrepare(StatementHandler sh,Connection connection,Integer transactionTimeout){InnerInterceptor.super.beforePrepare(sh, connection, transactionTimeout);}@OverrideprotectedvoidprocessDelete(Delete delete,int index,String sql,Object obj){super.processDelete(delete, index, sql, obj);}@OverrideprotectedvoidprocessUpdate(Update update,int index,String sql,Object obj){super.processUpdate(update, index, sql, obj);}@OverrideprotectedvoidprocessSelect(Select select,int index,String sql,Object obj){SelectBody selectBody = select.getSelectBody();try{// 单个sqlif(selectBody instanceofPlainSelect){this.setWhere((PlainSelect) selectBody, obj.toString());}elseif(selectBody instanceofSetOperationList){// 多个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();}}protectedvoidsetWhere(PlainSelect plainSelect,String mapperId){Expression sqlSegment = mybatisPlusPermissionHandler.getSqlSegment(plainSelect.getWhere(), mapperId);if(null!= sqlSegment){
plainSelect.setWhere(sqlSegment);}}
(4).Mybatis-Plus处理器
这里就是处理数据权限的主要类,来动态拼接sql的条件,达到数据隔离的效果
importcom.baymax.enums.DataScope;importcom.baymax.utils.SecurityUserUtils;importnet.sf.jsqlparser.expression.Expression;importnet.sf.jsqlparser.expression.Parenthesis;importnet.sf.jsqlparser.expression.operators.conditional.AndExpression;importnet.sf.jsqlparser.parser.CCJSqlParserUtil;/**
* 数据权限处理器
*
* @author baymax
* @date 2023-02-09 15:50
*/publicclassMybatisPlusPermissionHandler{/**
* 拼接数据权限sql的条件
*
* @param where sql语句的where条件
* @param mapperId mapper执行的方法
*/publicExpressiongetSqlSegment(Expression where,String mapperId){//TODO 如果是管理员,拥有全部数据权限,不做任何条件语句的拼接,直接返回未处理的whereif(管理员){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 =newParenthesis(expression);if(null!= where){returnnewAndExpression(where, parenthesis);}else{return parenthesis;}}catch(Exception e){thrownewRuntimeException("数据权限解析异常 => "+ e.getMessage());}}/**
* 非常重要!!!
* 拼接数据权限sql的条件,比如在我们登录成功时,会把用户、角色、数据权限信息缓存到容器上下文或者redis中,当非管理员用户访问某个接口时有查询一些数据,最后会到这个来,这时候我们可以从容器上下文或redis中拿到当前用户的信息,判断用户的数据权限,不同数据权限走不同的拼接SQL
*/privateStringbuildDataFilter(){StringBuilder stringBuilder =newStringBuilder();DataScope dataScope ="拿当前用户的数据权限";// 部门数据权限if(DataScope.DEPARTMENT== dataScope){// 例如:可以返回 "department_id IN (当前用户所属的部门id)"return"";}elseif(DataScope.SELF== dataScope){// 仅本人数据权限,create_by字段是表中的字段,这里不一定是这个字段,根据自己需求在需要做数据权限的表加上自定义得字段
stringBuilder.append(" create_by").append("= '").append("拿当前用户的username").append("'");return stringBuilder.toString();}return"";}
注意:这里省略了登录的流程,这个自行实现
3.测试
当前登录的用户为:zzh
(1).即将查询的表数据
(2).查询的结果
版权归原作者 搬砖爱好者. 所有, 如有侵权,请联系我们删除。