如何利用MyBatis Plus去实现数据权限控制呢?

前言背景

平时开发中遇到根据当前用户的角色,只能查看数据权限范围的数据需求。列表实现方案有两种,一是在开发初期就做好判断赛选,但如果这个需求是中途加的,或不希望每个接口都加一遍,就可以方案二加拦截器的方式。在mybatis执行sql前修改语句,限定where范围。

当然拦截器生效后是全局性的,如何保证只对需要的接口进行拦截和转化,就可以应用注解进行识别

因此具体需要哪些步骤就明确了

创建注解类

创建拦截器实现InnerInterceptor接口,重写查询方法

创建处理类,获取数据权限 SQL 片段,设置where

将拦截器加到MyBatis-Plus插件中

自定义注解

importjava.lang.annotation.ElementType;importjava.lang.annotation.Retention;importjava.lang.annotation.RetentionPolicy;importjava.lang.annotation.Target;@Target({ElementType.METHOD,ElementType.TYPE})@Retention(RetentionPolicy.RUNTIME)public@interfaceUserDataPermission{}

拦截器

importcom.baomidou.mybatisplus.core.plugins.InterceptorIgnoreHelper;importcom.baomidou.mybatisplus.core.toolkit.PluginUtils;importcom.baomidou.mybatisplus.extension.parser.JsqlParserSupport;importcom.baomidou.mybatisplus.extension.plugins.inner.InnerInterceptor;importlombok.*;importnet.sf.jsqlparser.expression.Expression;importnet.sf.jsqlparser.statement.select.PlainSelect;importnet.sf.jsqlparser.statement.select.Select;importnet.sf.jsqlparser.statement.select.SelectBody;importnet.sf.jsqlparser.statement.select.SetOperationList;importorg.apache.ibatis.executor.Executor;importorg.apache.ibatis.mapping.BoundSql;importorg.apache.ibatis.mapping.MappedStatement;importorg.apache.ibatis.session.ResultHandler;importorg.apache.ibatis.session.RowBounds;importjava.sql.SQLException;importjava.util.List;@Data@NoArgsConstructor@AllArgsConstructor@ToString(callSuper=true)@EqualsAndHashCode(callSuper=true)publicclassMyDataPermissionInterceptorextendsJsqlParserSupportimplementsInnerInterceptor{/***数据权限处理器*/privateMyDataPermissionHandlerdataPermissionHandler;@OverridepublicvoidbeforeQuery(Executorexecutor,MappedStatementms,Objectparameter,RowBoundsrowBounds,ResultHandlerresultHandler,BoundSqlboundSql)throwsSQLException{if(InterceptorIgnoreHelper.willIgnoreDataPermission(ms.getId())){return;}PluginUtils.MPBoundSqlmpBs=PluginUtils.mpBoundSql(boundSql);mpBs.sql(this.parserSingle(mpBs.sql(),ms.getId()));}@OverrideprotectedvoidprocessSelect(Selectselect,intindex,Stringsql,Objectobj){SelectBodyselectBody=select.getSelectBody();if(selectBodyinstanceofPlainSelect){this.setWhere((PlainSelect)selectBody,(String)obj);}elseif(selectBodyinstanceofSetOperationList){SetOperationListsetOperationList=(SetOperationList)selectBody;ListselectBodyList=setOperationList.getSelects();selectBodyList.forEach(s->this.setWhere((PlainSelect)s,(String)obj));}}/***设置where条件**@paramplainSelect查询对象*@paramwhereSegment查询条件片段*/privatevoidsetWhere(PlainSelectplainSelect,StringwhereSegment){ExpressionsqlSegment=this.dataPermissionHandler.getSqlSegment(plainSelect,whereSegment);if(null!=sqlSegment){plainSelect.setWhere(sqlSegment);}}}

拦截器处理器

基础只涉及 = 表达式,要查询集合范围 in 看进阶版用例

importcn.hutool.core.collection.CollectionUtil;importlombok.SneakyThrows;importlombok.extern.slf4j.Slf4j;importnet.sf.jsqlparser.expression.Alias;importnet.sf.jsqlparser.expression.Expression;importnet.sf.jsqlparser.expression.HexValue;importnet.sf.jsqlparser.expression.StringValue;importnet.sf.jsqlparser.expression.operators.conditional.AndExpression;importnet.sf.jsqlparser.expression.operators.relational.EqualsTo;importnet.sf.jsqlparser.expression.operators.relational.ExpressionList;importnet.sf.jsqlparser.expression.operators.relational.InExpression;importnet.sf.jsqlparser.expression.operators.relational.ItemsList;importnet.sf.jsqlparser.schema.Column;importnet.sf.jsqlparser.schema.Table;importnet.sf.jsqlparser.statement.select.PlainSelect;importjava.lang.reflect.Method;importjava.util.List;importjava.util.Objects;importjava.util.Set;importjava.util.stream.Collectors;@Slf4jpublicclassMyDataPermissionHandler{/***获取数据权限SQL片段**@paramplainSelect查询对象*@paramwhereSegment查询条件片段*@returnJSqlParser条件表达式*/@SneakyThrows(Exception.class)publicExpressiongetSqlSegment(PlainSelectplainSelect,StringwhereSegment){//待执行SQLWhere条件表达式Expressionwhere=plainSelect.getWhere();if(where==null){where=newHexValue("1=1");}log.info("开始进行权限过滤,where:{},mappedStatementId:{}",where,whereSegment);//获取mapper名称StringclassName=whereSegment.substring(0,whereSegment.lastIndexOf("."));//获取方法名StringmethodName=whereSegment.substring(whereSegment.lastIndexOf(".")+1);TablefromItem=(Table)plainSelect.getFromItem();//有别名用别名,无别名用表名,防止字段冲突报错AliasfromItemAlias=fromItem.getAlias();StringmainTableName=fromItemAlias==null?fromItem.getName():fromItemAlias.getName();//获取当前mapper的方法Method[]methods=Class.forName(className).getMethods();//遍历判断mapper的所以方法,判断方法上是否有UserDataPermissionfor(Methodm:methods){if(Objects.equals(m.getName(),methodName)){UserDataPermissionannotation=m.getAnnotation(UserDataPermission.class);if(annotation==null){returnwhere;}//1、当前用户CodeUseruser=SecurityUtils.getUser();//查看自己的数据//=表达式EqualsTousesEqualsTo=newEqualsTo();usesEqualsTo.setLeftExpression(newColumn(mainTableName+".creator_code"));usesEqualsTo.setRightExpression(newStringValue(user.getUserCode()));returnnewAndExpression(where,usesEqualsTo);}}//说明无权查看,where=newHexValue("1=2");returnwhere;}}

将拦截器加到MyBatis-Plus插件中

如果你之前项目配插件 ,直接用下面方式就行

@BeanpublicMybatisPlusInterceptormybatisPlusInterceptor(){MybatisPlusInterceptorinterceptor=newMybatisPlusInterceptor();//添加数据权限插件MyDataPermissionInterceptordataPermissionInterceptor=newMyDataPermissionInterceptor();//添加自定义的数据权限处理器dataPermissionInterceptor.setDataPermissionHandler(newMyDataPermissionHandler());interceptor.addInnerInterceptor(dataPermissionInterceptor);interceptor.addInnerInterceptor(newPaginationInnerInterceptor(DbType.MYSQL));returninterceptor;}

但如果你项目之前是依赖包依赖,或有公司内部统一拦截设置好,也可以往MybatisPlusInterceptor进行插入,避免影响原有项目配置

@BeanpublicMyDataPermissionInterceptormyInterceptor(MybatisPlusInterceptormybatisPlusInterceptor){MyDataPermissionInterceptorsql=newMyDataPermissionInterceptor();sql.setDataPermissionHandler(newMyDataPermissionHandler());Listlist=newArrayList();//添加数据权限插件list.add(sql);//分页插件mybatisPlusInterceptor.setInterceptors(list);list.add(newPaginationInnerInterceptor(DbType.MYSQL));returnsql;}

以上就是简单版的是拦截器修改语句使用

使用方式

在mapper层添加注解即可

@UserDataPermissionListselectAllCustomerPage(IPagepage,@Param("customerName")StringcustomerName);

基础班只是能用,业务功能没有特别约束,先保证能跑起来

进阶版 解决两个问题:

加了角色,用角色决定范围

解决不是mapper层自定义sql查询问题。

两个是完全独立的问题 ,可根据情况分开解决

解决不是mapper层自定义sql查询问题。

例如我们名称简单的sql语句 直接在Service层用mybatisPluse自带的方法

xxxxService.list(WrapperqueryWrapper)xxxxService.page(newPage(),WrapperqueryWrapper)

以上这种我应该把注解加哪里呢

因为service层,本质上还是调mapper层, 所以还是在mapper层做文章,原来的mapper实现了extends BaseMapper 接口,所以能够查询,我们要做的就是在 mapper层中间套一个中间接口,来方便我们加注解

xxxxxMapper——》DataPermissionMapper(中间)——》BaseMapper

根据自身需要,在重写的接口方法上加注解即可,这样就影响原先的代码

importcom.baomidou.mybatisplus.core.conditions.Wrapper;importcom.baomidou.mybatisplus.core.mapper.BaseMapper;importcom.baomidou.mybatisplus.core.metadata.IPage;importcom.baomidou.mybatisplus.core.toolkit.Constants;importorg.apache.ibatis.annotations.Param;importjava.io.Serializable;importjava.util.Collection;importjava.util.List;importjava.util.Map;publicinterfaceDataPermissionMapperextendsBaseMapper{/***根据ID查询**@paramid主键ID*/@Override@UserDataPermissionTselectById(Serializableid);/***查询(根据ID批量查询)**@paramidList主键ID列表(不能为null以及empty)*/@Override@UserDataPermissionListselectBatchIds(@Param(Constants.COLLECTION)CollectionidList);/***查询(根据columnMap条件)**@paramcolumnMap表字段map对象*/@Override@UserDataPermissionListselectByMap(@Param(Constants.COLUMN_MAP)MapcolumnMap);/***根据entity条件,查询一条记录**@paramqueryWrapper实体对象封装操作类(可以为null)*/@Override@UserDataPermissionTselectOne(@Param(Constants.WRAPPER)WrapperqueryWrapper);/***根据Wrapper条件,查询总记录数**@paramqueryWrapper实体对象封装操作类(可以为null)*/@Override@UserDataPermissionIntegerselectCount(@Param(Constants.WRAPPER)WrapperqueryWrapper);/***根据entity条件,查询全部记录**@paramqueryWrapper实体对象封装操作类(可以为null)*/@Override@UserDataPermissionListselectList(@Param(Constants.WRAPPER)WrapperqueryWrapper);/***根据Wrapper条件,查询全部记录**@paramqueryWrapper实体对象封装操作类(可以为null)*/@Override@UserDataPermissionList<Map>selectMaps(@Param(Constants.WRAPPER)WrapperqueryWrapper);/***根据Wrapper条件,查询全部记录*

注意:只返回第一个字段的值

**@paramqueryWrapper实体对象封装操作类(可以为null)*/@Override@UserDataPermissionListselectObjs(@Param(Constants.WRAPPER)WrapperqueryWrapper);/***根据entity条件,查询全部记录(并翻页)**@parampage分页查询条件(可以为RowBounds.DEFAULT)*@paramqueryWrapper实体对象封装操作类(可以为null)*/@Override@UserDataPermission<E extends IPage>EselectPage(Epage,@Param(Constants.WRAPPER)WrapperqueryWrapper);/***根据Wrapper条件,查询全部记录(并翻页)**@parampage分页查询条件*@paramqueryWrapper实体对象封装操作类*/@Override@UserDataPermission<E extends IPage<Map>>EselectMapsPage(Epage,@Param(Constants.WRAPPER)WrapperqueryWrapper);}

解决角色控制查询范围

引入角色,我们先假设有三种角色,按照常规的业务需求,一种是管理员查看全部、一种是部门管理查看本部门、一种是仅查看自己。

有了以上假设,就可以设置枚举类编写业务逻辑, 对是业务逻辑,所以我们只需要更改”拦截器处理器类“

建立范围枚举

建立角色枚举以及范围关联关系

重写拦截器处理方法

范围枚举

@AllArgsConstructor@GetterpublicenumDataScope{//Scope数据权限范围:ALL(全部)、DEPT(部门)、MYSELF(自己)ALL("ALL"),DEPT("DEPT"),MYSELF("MYSELF");privateStringname;}

角色枚举

@AllArgsConstructor@GetterpublicenumDataPermission{//枚举类型根据范围从前往后排列,避免影响getScope//Scope数据权限范围:ALL(全部)、DEPT(部门)、MYSELF(自己)DATA_MANAGER("数据管理员","DATA_MANAGER",DataScope.ALL),DATA_AUDITOR("数据审核员","DATA_AUDITOR",DataScope.DEPT),DATA_OPERATOR("数据业务员","DATA_OPERATOR",DataScope.MYSELF);privateStringname;privateStringcode;privateDataScopescope;publicstaticStringgetName(Stringcode){for(DataPermissiontype:DataPermission.values()){if(type.getCode().equals(code)){returntype.getName();}}returnnull;}publicstaticStringgetCode(Stringname){for(DataPermissiontype:DataPermission.values()){if(type.getName().equals(name)){returntype.getCode();}}returnnull;}publicstaticDataScopegetScope(Collectioncode){for(DataPermissiontype:DataPermission.values()){for(Stringv:code){if(type.getCode().equals(v)){returntype.getScope();}}}returnDataScope.MYSELF;}}

重写拦截器处理类 MyDataPermissionHandler

importlombok.SneakyThrows;importlombok.extern.slf4j.Slf4j;importnet.sf.jsqlparser.expression.Alias;importnet.sf.jsqlparser.expression.Expression;importnet.sf.jsqlparser.expression.HexValue;importnet.sf.jsqlparser.expression.StringValue;importnet.sf.jsqlparser.expression.operators.conditional.AndExpression;importnet.sf.jsqlparser.expression.operators.relational.EqualsTo;importnet.sf.jsqlparser.expression.operators.relational.ExpressionList;importnet.sf.jsqlparser.expression.operators.relational.InExpression;importnet.sf.jsqlparser.expression.operators.relational.ItemsList;importnet.sf.jsqlparser.schema.Column;importnet.sf.jsqlparser.schema.Table;importnet.sf.jsqlparser.statement.select.PlainSelect;importjava.lang.reflect.Method;importjava.util.List;importjava.util.Objects;importjava.util.Set;importjava.util.stream.Collectors;@Slf4jpublicclassMyDataPermissionHandler{privateRemoteRoleServiceremoteRoleService;privateRemoteUserServiceremoteUserService;/***获取数据权限SQL片段**@paramplainSelect查询对象*@paramwhereSegment查询条件片段*@returnJSqlParser条件表达式*/@SneakyThrows(Exception.class)publicExpressiongetSqlSegment(PlainSelectplainSelect,StringwhereSegment){remoteRoleService=SpringUtil.getBean(RemoteRoleService.class);remoteUserService=SpringUtil.getBean(RemoteUserService.class);//待执行SQLWhere条件表达式Expressionwhere=plainSelect.getWhere();if(where==null){where=newHexValue("1=1");}log.info("开始进行权限过滤,where:{},mappedStatementId:{}",where,whereSegment);//获取mapper名称StringclassName=whereSegment.substring(0,whereSegment.lastIndexOf("."));//获取方法名StringmethodName=whereSegment.substring(whereSegment.lastIndexOf(".")+1);TablefromItem=(Table)plainSelect.getFromItem();//有别名用别名,无别名用表名,防止字段冲突报错AliasfromItemAlias=fromItem.getAlias();StringmainTableName=fromItemAlias==null?fromItem.getName():fromItemAlias.getName();//获取当前mapper的方法Method[]methods=Class.forName(className).getMethods();//遍历判断mapper的所以方法,判断方法上是否有UserDataPermissionfor(Methodm:methods){if(Objects.equals(m.getName(),methodName)){UserDataPermissionannotation=m.getAnnotation(UserDataPermission.class);if(annotation==null){returnwhere;}//1、当前用户CodeUseruser=SecurityUtils.getUser();//2、当前角色即角色或角色类型(可能多种角色)SetroleTypeSet=remoteRoleService.currentUserRoleType();DataScopescopeType=DataPermission.getScope(roleTypeSet);switch(scopeType){//查看全部caseALL:returnwhere;caseDEPT://查看本部门用户数据//创建IN表达式//创建IN范围的元素集合ListdeptUserList=remoteUserService.listUserCodesByDeptCodes(user.getDeptCode());//把集合转变为JSQLParser需要的元素列表ItemsListdeptList=newExpressionList(deptUserList.stream().map(StringValue::new).collect(Collectors.toList()));InExpressioninExpressiondept=newInExpression(newColumn(mainTableName+".creator_code"),deptList);returnnewAndExpression(where,inExpressiondept);caseMYSELF://查看自己的数据//=表达式EqualsTousesEqualsTo=newEqualsTo();usesEqualsTo.setLeftExpression(newColumn(mainTableName+".creator_code"));usesEqualsTo.setRightExpression(newStringValue(user.getUserCode()));returnnewAndExpression(where,usesEqualsTo);default:break;}}}//说明无权查看,where=newHexValue("1=2");returnwhere;}}

以上就是全篇知识点, 需要注意的点可能有:

记得把拦截器加到MyBatis-Plus的插件中,确保生效

要有一个业务赛选标识字段, 这里用的创建人 creator_code, 也可以用dept_code 等等。

版权声明:
作者:网友投稿
链接:http://qiangcao.com/zs/1812.html
来源:轩哥技术分享
文章版权归作者所有,未经允许请勿转载。

THE END
分享
二维码
< <上一篇
下一篇>>