Mybatis-plus拦截器BaseMultiTableInnerInterceptor实现(使用场景)
Mybatis-plus拦截器BaseMultiTableInnerInterceptor数据权限(使用场景)
前言
前段时间接到需求,某某业务系统上线一年有余 ,但是目前未作数据隔离,所有账号看到的数据都是一样的,系统设计初期如此,现在组织变更存在多个分子公司,组织树变壮大,需要做数据隔离;让不同的人员不同的角色通过设置权限看到的数据不一样,考虑到目前系统已经上线,此次改造还需要慎重考虑。
由于系统上线已久,业务接口颇多,不可能通过代码修改每个接口对数据进行过滤操作数据,工作量太大了,只能在最终执行的SQL语句处进行拦截,而如果每个语句都特殊处理的话也是个巨大的工程,之前刚好接触过一点Mybatis-plus拦截器的使用,此次改造主要用此拦截器结合其他技术实现,应用场景和多租户的动态拦截拼接SQL一样,简单来说就是在最终执行的SQL上在拼接上需要增加的数据范围的SQL语句;
此处使用BaseMultiTableInnerInterceptor主要是因为报表等模块复杂SQL问题,若只是简单的SQL拦截处理,使用继承JsqlParserSupport也可用,后续有时间也会写此方法的使用情况(如租户隔离);
BaseMultiTableInnerInterceptor对mybatis_plus版本要求较高(3.5.3.1);
方案设计
-
目前此系统的数据范围权限是人员所属的角色可控制各个模块能看到的数据范围(如图),本质是存储的就是对应人员所属的角色的相关的组织机构id;如设置本分子公司及其以下就是存储的这个这个分子公司下的所有组织机构id;所以后期拦截器就是为了把这些返回的orgId拼接在最终执行的SQL语句后再去执行;
//改造前sql语句 select * from material_dictionary t1 left join material_category t2 on t1.category_id =t2.id where t1.tenant_id ='1111'; //改造后拦截后sql语句 select * from material_dictionary t1 left join material_category t2 on t1.category_id =t2.id where t1.tenant_id ='1111' and t1.orgid in('1','2','3') and t2.orgid in('1','2','3') ;
-
此数据范围是平台的公告能力,但是业务系统是基于平台开发的业务模块,业务模块之前没使用到平台的数据范围数据;
-
前期考虑问题
-
Mybatis-plus拦截器会拦截所有的SQL语句?那是否需要所有的都拦截拼接数据范围的语句呢?那些需要放行,那些需要拦截 ??
-
获取查询以上平台的数据范围返回的数据(获取时机问题需要考虑,不能等sql执行的时候一直等着去查询平台的数据范围,导致整个查询变慢);
-
获取平台数据范围是通过redis缓存下 ,还是http请求时就拦截缓存到某处(线程局部变量ThreadLocal);
…
带着这些问题,开始设计了…
-
-
设计思路
-
所有业务表增加相关数据范围字段;
-
http拦截器拦截前端所有的http请求(业务接口)
-
获取此次http请求模块平台对应的用户数据范围,存线程局部变量ThreadLocal;
-
业务接口到达mybatis_plus数据范围拦截器拦截,加载ThreadLocal中的用户数据范围,构建动态sql(拼接sql);
-
层层返回结果;
注:以上只是理想化的使用场景,实际中还有未考虑到的场景,先设计再二次改造;
-
-
设计图
实现过程
-
数据库表增加数据权限表字段
-
名称 编码 类型 说明 租户id tenant_id varchar(32) 当前数据归属租户ID 组织id org_id varchar(32) 创建当前数据的组织ID 组织名称 org_name varchar(128) 创建当前数据的组织名称 分子公司id company_id varchar(32) 分子公司ID,取orgId上级分子公司 分子公司名称 company_name varchar(128) 分子公司名称,取orgId上级分子公司 创建人 create_user_id varchar(36) 创建人 ----------- 批量生成所有表需要增加的字段语句---- SELECT CONCAT('ALTER TABLE ', table_name, 'ADD COLUMN orgId TO org_id, ADD COLUMN orgName TO org_name, ADD COLUMN companyId TO company_id , ADD COLUMN companyName TO company_name , ADD COLUMN createUserId TO create_user_id, ') FROM information_schema.tables WHERE table_schema = 'zcgl_dev'; -- 数据库名
如图所示,导出粘贴出来直接执行
-
-
字段增加索引
--给所有表生成索引字段如下 select CONCAT( 'ALTER TABLE ', table_name, ' ADD INDEX idx_', '_org_id (org_id); ', 'ALTER TABLE ', table_name, ' ADD INDEX idx_', '_company_id (company_id) ;' ) from ( SELECT DISTINCT table_name FROM information_schema.columns WHERE table_schema = DATABASE() AND table_name NOT IN ('temp_tables') ) t;
-
web请求拦截器
配置web端请求拦截器,拦截所有的web请求,获取数据范围信息存,
import lombok.extern.slf4j.Slf4j; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.web.servlet.HandlerInterceptor; import javax.annotation.Resource; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.util.Arrays; /** * 数据权限上下文拦截器 */ @Slf4j public class DataPermissionRequestInterceptor implements HandlerInterceptor { private final static Logger logger = LoggerFactory.getLogger(DataPermissionRequestInterceptor.class); //数据范围过滤 private final static String DATA_SCOPE_KEY = "dataScope"; @Resource private DataScopeManager dataScopeManager; @Resource private DataPermissionContext dataPermissionContext; @Resource private DataPermissionServiceFacade dataPermissionServiceFacade; public DataPermissionRequestInterceptor() { } @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { try { this.doPre(request); } catch (Throwable var5) { logger.error(var5.getMessage(), var5); } return true; } private void doPre(HttpServletRequest request) { //获取数据范围 loadDataPermissionContext(request); //判断是否数据过滤 String[] parameterValues = request.getParameterValues(DATA_SCOPE_KEY); if (parameterValues != null) { long count = Arrays.stream(parameterValues).filter("true"::equals).count(); if (count > 0) { dataScopeManager.push(true); }else{ dataScopeManager.push(false); } } } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { if (!request.isAsyncStarted()) { this.dataScopeManager.clear(); this.dataPermissionContext.clear(); } } /** * 初始化数据范围上下文 */ private void loadDataPermissionContext(HttpServletRequest request) { DataPermissionEntity dataScope = dataPermissionServiceFacade.getDataPermissionEntity(); if (dataScope != null) { this.dataPermissionContext.setDataPermissionEntity(dataScope); } else if (!request.isAsyncStarted()) { this.dataPermissionContext.clear(); } } }
public interface DataPermissionServiceFacade { // 获取数据权限范围 DataPermissionEntity getDataPermissionEntity(); }
获取数据范围的业务代码,看下大概逻辑返回,不需要多关注
import lombok.extern.slf4j.Slf4j; import org.apache.commons.collections.CollectionUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import java.util.Collection; import java.util.List; import java.util.stream.Collectors; /** 获取数据范围的业务代码不需要多关注 */ @Component @Slf4j public class DataPermissionServiceFacadeImpl implements DataPermissionServiceFacade { @Autowired private SysAuthObjectDetailClient sysAuthObjectDetailClient; @Autowired private OrgContext orgContext; @Autowired private UserContext userContext; @Autowired private SysOrgClient sysOrgClient; @Autowired private ProductContext productContext; @Autowired private CurrentUserClient currentUserClient; @Override public DataPermissionEntity getDataPermissionEntity() { QueryTreeOrgReq queryTreeOrgReq = new QueryTreeOrgReq(); queryTreeOrgReq.setOrgLevel(OrgLevelEnum.GROUP.getCode()); return queryDataPermissionEntity(queryTreeOrgReq); } /** * 获取数据范围 */ public DataPermissionEntity queryDataPermissionEntity(QueryTreeOrgReq queryTreeOrgReq) { log.info("queryDataPermissionEntity: modelId={}, userId={}, orgId={}",productContext.getModeId(), userContext.getId(), orgContext.getId()); DataPermissionEntity dataPermissionEntity = new DataPermissionEntity(); if (currentUserClient.isTenantAdmin()) { return dataPermissionEntity; } // 没有模块ID不查询数据范围 if (org.apache.commons.lang3.StringUtils.isEmpty(productContext.getModeId())) { return dataPermissionEntity; } // 查询当前用户当前模块的数据范围 List<AuthDataModel> authDataModels = sysAuthObjectDetailClient.queryModelDataAuthList(productContext.getModeId()); log.info("queryDataPermissionEntity:authDataModels={}", authDataModels); if (CollectionUtils.isEmpty(authDataModels)) { return dataPermissionEntity; } // 授权列表中不存在数据范围权限的,展示全部 long count = authDataModels.stream().filter(item -> !DataAuthCodeEnum.NO_AUTH.getCode().equals(item.getCode())).count(); if (count <= 0) { long noAuthCount = authDataModels.stream().filter(item -> DataAuthCodeEnum.NO_AUTH.getCode().contains(item.getCode())).count(); dataPermissionEntity.setSysOrgModelList(sysOrgClient.tree(queryTreeOrgReq)); if(noAuthCount > 0){ dataPermissionEntity.setNotAuth(true); } return dataPermissionEntity; } // 授权列表中存在数据范围的,以存在的范围为准 List<String> orgIds = authDataModels.stream().filter(item -> !DataAuthCodeEnum.SELF.getCode().equals(item.getCode())) .map(AuthDataModel::getData).flatMap(Collection::stream).collect(Collectors.toList()); if (!org.springframework.util.CollectionUtils.isEmpty(orgIds)) { for (AuthDataModel item : authDataModels) { boolean isQueryAll = isQueryGroupData(item); if(isQueryAll){ dataPermissionEntity.setQueryAll(true); return dataPermissionEntity; } } dataPermissionEntity.setOrgIds(orgIds); } // 授权列表中存在本人的,查询本人创建的组织 List<String> userIds = authDataModels.stream().filter(item -> DataAuthCodeEnum.SELF.getCode().equals(item.getCode())) .map(AuthDataModel::getData).flatMap(Collection::stream).collect(Collectors.toList()); if (!org.springframework.util.CollectionUtils.isEmpty(userIds)) { dataPermissionEntity.setUserIds(userIds); } return dataPermissionEntity; } /** * 是否查询全公司数据 * @param item * @return */ private boolean isQueryGroupData(AuthDataModel item) { String authCode = item.getCode(); if (DataAuthCodeEnum.ORGANIZATION_RANGE.getCode().equals(authCode) || DataAuthCodeEnum.THE_ORGANIZATION_CHILDREN.getCode().equals(authCode)) { List<String> data = item.getData(); if(CollectionUtils.isNotEmpty(data)){ return data.contains("1"); } } return false; } }
-
webconfig配置类
配置web端请求拦截器,拦截所有的web请求,获取数据范围信息存,
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; @Configuration public class AuthDataWebMvcConfig implements WebMvcConfigurer { @Bean public DataPermissionContext dataPermissionContext() { return DefaultDataPermissionContext.newDataUserContext(); } @Bean public DataPermissionRequestInterceptor authDataModelsInterceptor() { return new DataPermissionRequestInterceptor(); } @Override public void addInterceptors(InterceptorRegistry registry) { //拦截前端所有请求 , order(2)是因为原来已经存在WebMvcConfig的配置类,此处需要优先于平台的执行 registry.addInterceptor(authDataModelsInterceptor()).addPathPatterns("/**").order(2) ; } /** * 数据范围上下文默认实现 */ private static class DefaultDataPermissionContext implements DataPermissionContext { //数据范围信息 private static ThreadLocal<DataPermissionEntity> authCurrentUserOrgThreadLocal = new ThreadLocal<>(); public static DefaultDataPermissionContext newDataUserContext() { return new DefaultDataPermissionContext(); } private DefaultDataPermissionContext(){ } @Override public DataPermissionEntity getDataPermissionEntity() { return authCurrentUserOrgThreadLocal.get(); } @Override public void setDataPermissionEntity(DataPermissionEntity dataPermissionEntity) { authCurrentUserOrgThreadLocal.set(dataPermissionEntity); } @Override public void clear() { authCurrentUserOrgThreadLocal.remove(); } } }
-
数据范围上下文对象
/** * 收据范围上下文 */ public interface DataPermissionContext { void setDataPermissionEntity(DataPermissionEntity dataPermissionEntity); DataPermissionEntity getDataPermissionEntity(); void clear(); }
-
数据范围授权实体
import lombok.Data; import java.util.List; /** * 收据范围授权信息 */ @Data public class DataPermissionEntity { private List<SysOrgModel> sysOrgModelList; /** * 无权限 */ private boolean notAuth; /** * 查询全部 * 公司及以下||自定义范围是公司的 */ private boolean queryAll; /** * 组织ID */ private List<String> orgIds; /** * 用户ID */ private List<String> userIds; }
-
MybatisPlusConfig配置类
配置Mybatis Plus拦截器,数据权限handler作为参数传给拦截器构造方法。
import com.baomidou.mybatisplus.annotation.DbType; import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; import com.baomidou.mybatisplus.extension.plugins.inner.*; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Primary; import org.springframework.core.annotation.Order; import javax.annotation.Resource; @Configuration public class MybatisPlusConfig { @Resource private DataPermissionSqlInterceptor dataPermissionSqlInterceptor; @Bean @Order(1) @Primary public MybatisPlusInterceptor mybatisPlusInterceptor(DefaultUpdateInterceptor defaultUpdateInterceptor, NepochTenantHandler nepochTenantHandler) { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(dataPermissionSqlInterceptor); //分页插件,拦截器会导致原始分页肯可鞥失效,需要在处理下 interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor()); interceptor.addInnerInterceptor(new BlockAttackInnerInterceptor()); return interceptor; } /** * 分页插件 * @return */ @Bean public PaginationInnerInterceptor paginationInterceptor() { return new PaginationInnerInterceptor(); } }
-
mybatis_plus拦截处理拼接sql
核心的方法:
* select语句进processSelect()
* 拼接构造语句builderExpression()import cn.hutool.core.util.BooleanUtil; import com.baomidou.mybatisplus.core.plugins.InterceptorIgnoreHelper; import com.baomidou.mybatisplus.core.toolkit.CollectionUtils; import com.baomidou.mybatisplus.core.toolkit.PluginUtils; import com.baomidou.mybatisplus.extension.plugins.inner.BaseMultiTableInnerInterceptor; import lombok.extern.slf4j.Slf4j; import net.sf.jsqlparser.expression.Expression; import net.sf.jsqlparser.expression.Parenthesis; import net.sf.jsqlparser.expression.operators.conditional.AndExpression; import net.sf.jsqlparser.expression.operators.conditional.OrExpression; import net.sf.jsqlparser.expression.operators.relational.EqualsTo; import net.sf.jsqlparser.expression.operators.relational.InExpression; import net.sf.jsqlparser.schema.Column; import net.sf.jsqlparser.schema.Table; import net.sf.jsqlparser.statement.select.Select; import net.sf.jsqlparser.statement.select.WithItem; 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.mapping.SqlCommandType; import org.apache.ibatis.session.ResultHandler; import org.apache.ibatis.session.RowBounds; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; import javax.annotation.Resource; import java.sql.Connection; import java.sql.SQLException; import java.util.Arrays; import java.util.List; import java.util.Objects; import java.util.stream.Collectors; @Slf4j @Component public class DataPermissionSqlInterceptor extends BaseMultiTableInnerInterceptor implements InnerInterceptor { /** * *用在数据范围筛选的用户id字段的默认字段名称 */ public static final String DEFAULT_USER_ID_FIELD_NAME = "create_user_id"; /** * 用在数据范围筛选的组织1d字段的默认字段名称 */ public static final String DEFAULT_ORG_ID_FIELD_NAME = "org_id"; @Resource private DataScopeManager dataScopeManager; @Resource private DataPermissionContext dataScopeContext; @Value("${datatable.tableList}") private String[] tableList; @Override public void beforeQuery(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException { //没有打开数据范围开关 Boolean dataScope = dataScopeManager.peek(); if(!BooleanUtil.isTrue(dataScope)){ return; } //没有配置数据范围||数据范围设置的是分公司及以下(全部) DataPermissionEntity dataPermission = dataScopeContext.getDataPermissionEntity(); if (dataPermission == null || dataPermission.isQueryAll()) { return; } //mybatisPlus的忽略配置 if (!InterceptorIgnoreHelper.willIgnoreDataPermission(ms.getId())) { //执行sql PluginUtils.MPBoundSql mpBs = PluginUtils.mpBoundSql(boundSql); mpBs.sql(this.parserSingle(mpBs.sql(), (Object) null)); } } @Override public void beforePrepare(StatementHandler sh, Connection connection, Integer transactionTimeout) { PluginUtils.MPStatementHandler mpSh = PluginUtils.mpStatementHandler(sh); MappedStatement ms = mpSh.mappedStatement(); SqlCommandType sct = ms.getSqlCommandType(); if (sct == SqlCommandType.INSERT || sct == SqlCommandType.UPDATE || sct == SqlCommandType.DELETE) { //增删改查暂不处理 return; } } @Override protected void processSelect(Select select, int index, String sql, Object obj) { String whereSegment = (String) obj; this.processSelectBody(select.getSelectBody(), whereSegment); List<WithItem> withItemsList = select.getWithItemsList(); if (!CollectionUtils.isEmpty(withItemsList)) { withItemsList.forEach((withItem) -> { this.processSelectBody(withItem, whereSegment); }); } } /** 此处拼接条件 */ @Override protected Expression builderExpression(Expression currentExpression, List<Table> tables, final String whereSegment) { if (CollectionUtils.isEmpty(tables)) { return currentExpression; } else { Expression dataScopeTableExpression = this.buildTableExpression(tables.get(0), currentExpression, whereSegment); if (dataScopeTableExpression == null) { return currentExpression; } else { if (currentExpression == null) { return dataScopeTableExpression; } else { return currentExpression instanceof OrExpression ? new AndExpression(new Parenthesis(currentExpression), (Expression)dataScopeTableExpression) : new AndExpression(currentExpression, (Expression)dataScopeTableExpression); } } } } @Override public Expression buildTableExpression(final Table table, final Expression where, final String whereSegment) { return builderExpression(table); } /** * 处理条件 */ public Expression builderExpression(Table table) { // 判断该表是否在白名单中 ,不在则是业务表 ,需要拼orgId ,在则是系统表,不需要拼orgId if (Arrays.asList(tableList).contains(table.getName())) { return null; } //获取之前在ThreadLoca存的数据范围的数据 DataPermissionEntity dataScope = dataScopeContext.getDataPermissionEntity(); if (dataScope == null || ( CollectionUtils.isEmpty(dataScope.getUserIds()) && CollectionUtils.isEmpty(dataScope.getOrgIds()) )) { if (dataScope != null && BooleanUtil.isTrue(dataScope.isNotAuth())) { EqualsTo equalsTo = new EqualsTo(); equalsTo.setLeftExpression(new Column("'auth'")); equalsTo.setRightExpression(new Column("'noAuth'")); return equalsTo; } return null; } InExpression inExpression = new InExpression(); List<String> orgIds = dataScope.getOrgIds(); if (CollectionUtils.isNotEmpty(orgIds)) { //orgid in inExpression.setLeftExpression(this.getAliasColumn(table, DEFAULT_ORG_ID_FIELD_NAME)); //('2','212') inExpression.setRightExpression(this.getAliasRightColumn(orgIds)); } List<String> userIds = dataScope.getUserIds(); if (CollectionUtils.isNotEmpty(userIds)) { inExpression.setLeftExpression(this.getAliasColumn(table, DEFAULT_USER_ID_FIELD_NAME)); inExpression.setRightExpression(this.getAliasRightColumn(userIds)); } //inExpression实际就是orgid in ('2','212') return inExpression; } /** 处理条件后面拼接的值,orgid in ('1','2') */ protected Column getAliasRightColumn(List<String> list) { return new Column("(" + String.join(",", list) + ")"); } /** * 表别名设置 * org_id 或 tableAlias.org_id * @param table 表对象 * @return 字段 */ protected Column getAliasColumn(Table table, String columnName) { StringBuilder column = new StringBuilder(); if (table.getAlias() != null) { column.append(table.getAlias().getName()).append("."); } column.append(columnName); return new Column(column.toString()); } }
-
最终执行结果 :orgid in()
到这基本功能就算实现了 ,但是在实际测试中不同的接口有不同的要求 ,有些需要mybatis_plus拦截处理数据范围,有些不需要拦截,继续改造
改造过程
基于上面说到的有些需要拦截,有些不需要 ,不能一概而论;继续改造;
比如材料类别 ,供应商类别等都是系统公共数据 ,所有数据对全组织都可用 ,此时就不需要拦截拼接数据范围信息;
此处采用注解结合aop方式定义此接口是否需要mybatis_plus拦截处理数据范围;
-
定义注解
import java.lang.annotation.*; @Documented @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE, ElementType.METHOD}) public @interface DataScope { boolean value() default true; }
-
定义aop Aspect处理类
package com.glodon.nepoch.zcgl.dataScope.aspect; import com.glodon.nepoch.zcgl.dataScope.annotation.DataScope; import com.glodon.nepoch.zcgl.dataScope.context.DataScopeManager; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.Signature; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Pointcut; import org.aspectj.lang.reflect.MethodSignature; import org.springframework.stereotype.Component; import javax.annotation.Resource; import java.lang.reflect.Method; /** * 数据范围过滤 */ @Aspect @Component public class DataScopeAnnotationAspect { @Resource private DataScopeManager dataScopeManager; /** * 定义切点,切点为对应controller */ @Pointcut("@annotation(com.glodon.nepoch.zcgl.dataScope.annotation.DataScope)") public void aopPointCut(){ } @Around("aopPointCut()") public Object aroundMethod(ProceedingJoinPoint point) throws Throwable{ DataScope dataScope = getAnnotation(point); try { beforeProcess(dataScope); return methodProcess(point); } finally { afterProcess(dataScope); } } private static Object methodProcess(ProceedingJoinPoint point) throws Throwable { Object[] args = point.getArgs(); //获取参数 if(args != null){ return point.proceed(args); }else{ return point.proceed(); } } private void afterProcess(DataScope dataScope) { if (dataScope != null) { dataScopeManager.pop(); } } private void beforeProcess(DataScope dataScope) { if (dataScope != null) { //获取注解上的值 boolean open = dataScope.value(); dataScopeManager.push(open); } } //获取注解 public DataScope getAnnotation(ProceedingJoinPoint point) { Signature signature = point.getSignature(); MethodSignature methodSignature = (MethodSignature) signature; Method method = methodSignature.getMethod(); if (method != null){ return method.getAnnotation(DataScope.class); } return null; } }
-
定义DataScopeManager存储注解信息
import org.springframework.stereotype.Component; import java.util.Stack; @Component public class DataScopeManager { private final static ThreadLocal<Stack<Boolean>> DATA_SCOPE_STACK_THREAD_LOCAL = new ThreadLocal<>(); public void push(Boolean open) { Stack<Boolean> stack = DATA_SCOPE_STACK_THREAD_LOCAL.get(); if(stack == null){ DATA_SCOPE_STACK_THREAD_LOCAL.set(new Stack<>()); } DATA_SCOPE_STACK_THREAD_LOCAL.get().push(open); } public Boolean next() { pop(); return peek(); } public Boolean peek() { Stack<Boolean> stack = DATA_SCOPE_STACK_THREAD_LOCAL.get(); if (stack == null || stack.isEmpty()) { return null; } return stack.peek(); } public void pop() { Stack<Boolean> stack = DATA_SCOPE_STACK_THREAD_LOCAL.get(); if (stack == null || stack.isEmpty()) { return; } stack.pop(); } public void clear(){ DATA_SCOPE_STACK_THREAD_LOCAL.remove(); } }
-
Controller方法上增加注解
此处是在代码层增加注解,还可在接口中在在增加路径参数(如下)
@ApiOperation(value = "导出详情Excel", notes = "列表的导出-预损单详情") @PostMapping("/export/details") @DataScope public void exportDetailsExcel(@RequestBody( required = false) PreLossQueryDTO query, HttpServletResponse response) { preLossService.exportDetailsExcel(response, query); }
-
http接口中增加路径参数dataScope=true
-
http://127.0.0.1/md/listByPage?dataScope=true
-
http拦截器中增加路径参数判断
private void doPre(HttpServletRequest request) { //获取数据范围 loadDataPermissionContext(request); //判断是否接口中有数据过滤 String[] parameterValues = request.getParameterValues(DATA_SCOPE_KEY); if (parameterValues != null) { long count = Arrays.stream(parameterValues).filter("true"::equals).count(); if (count > 0) { dataScopeManager.push(true); }else{ dataScopeManager.push(false); } } }
-
-
mybatis_plus拦截处理数据范围代码beforeQuery方法中增加
//没有打开数据范围开关 Boolean dataScope = dataScopeManager.peek(); if(!BooleanUtil.isTrue(dataScope)){ return; }
总结
到此基本已经实现 ,满足使用场景 ,此次只是针对查询进行改造,所作的所有最终就是为了给查询条件后面拼接上其他条件,其他的增删改也原理相同;次此采用了注解、aop面向切面、http拦截器、ThreadLocal线程局部变量、mybatis_plus拦截器等实现了此功能,此处没有对mybatis_plus拦截器的源码做过多的说明 ,过程中用到的这些技术如若不熟悉,对此感兴趣的可以看其他博主的说明;此案例还是很具有代表性的功能 ,租户隔离 本此的权限后补等等;