## Mybatis实现方式
1.实现思想
利用Mybatis中提供的方法,可以实现SQL的提前拦截然后重新进行sql拼装的方式来让查询sql做到数据隔离的效果
2.具体实现过程
注:实现过程仅供参考,具体定义什么字段,如何实现根据实际业务进行定义
- 第一步是创建使用的注解,注解主要包含了作用表的别名 tableAlias(),以及默认所需维护的仓库库位id字段warehouseField()
/** * 数据权限控制注解 * 默认加上该注解会进行数据权限控制,相关业务不需要数据权限控制的话无需加上该注解 * 该注解使用在方法上 */ @Target({ElementType.METHOD,ElementType.PARAMETER}) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface FactoryDataScope { /** * 作用表的别名 */ String tableAlias() default ""; /** *仓库库位表字段名 */ String warehouseField() default "warehouse_field"; }
- 创建一个获取权限集合的工具类,具体如何实现根据自己的项目进行开发,最终实现的效果是获取如某个用户的权限集合
/** * 获取用户信息 * 用于获取用户的相关角色信息、权限信息 */ @Slf4j @Service public class WarehousePermissionUtils { @Autowired private AuthFeignClient authFeignClient; @Autowired private StoreFeignClients storeFeignClients; //获取用户权限集合 public Map<String, List<Integer>> getPermission(){ Map<String, List<Integer>> permissionMap = new HashMap<>(); //存放最终的所以仓库id集合 List<Integer> warehouseModels = new ArrayList<>(); //获取当前用户id Integer userId = BaseContextHandler.getUserID(); //根据当前用户id获取对应的数据角色集合 Result<List<Integer>> rolesByUserIds = authFeignClient.getRolesByUserIds(userId, 2); if(ObjectUtils.isNotEmpty(rolesByUserIds)){ rolesByUserIds.getData().stream().forEach(rolesByUserId->{ //获取当前用户所有的仓库权限 Result<List<AuthRoleWarehouse>> authRoleWarehouse = authFeignClient.getRoleWarehouse(rolesByUserId); List<Integer> warehouseIds = authRoleWarehouse.getData().stream().map(warehouse -> { return warehouse.getWarehouseId(); }).collect(Collectors.toList()); warehouseModels.addAll(warehouseIds); // warehouseIds.stream().forEach(warehouseId->{ // //获取当前仓库节点的所有子节点id // Result<List<Long>> WarehouseModelDtos = storeFeignClients.getByWarehouseModelId(Long.parseLong(String.valueOf(warehouseId))); // List<Integer> warehouseModelId = WarehouseModelDtos.getData().stream().map(warehouseModelDto -> { // return Integer.parseInt(String.valueOf(warehouseModelDto)); // }).collect(Collectors.toList()); // warehouseModels.addAll(warehouseModelId); // }); }); //有些父节点重复了,进行去重 List<Integer> result = warehouseModels.stream().distinct().collect(Collectors.toList()); permissionMap.put("warehouseModel",result); } return permissionMap; } }
- 编写一个数据过滤的配置类,主要实现方式是通过本地线程的相关特性来实现原有sql的替换,同时可以在获取到注解的同时做一些权限处理,如超级管理员的sql处理等
import com.baomidou.mybatisplus.extension.plugins.handler.DataPermissionHandler; import com.sw.mes.core.annotation.WarehouseDataScope; import com.sw.mes.core.context.BaseContextHandler; import com.sw.mes.core.utils.WarehousePermissionUtils; import lombok.AllArgsConstructor; import lombok.Data; import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; import net.sf.jsqlparser.expression.Expression; import net.sf.jsqlparser.expression.HexValue; import net.sf.jsqlparser.expression.LongValue; import net.sf.jsqlparser.expression.operators.conditional.AndExpression; import net.sf.jsqlparser.expression.operators.relational.ExpressionList; import net.sf.jsqlparser.expression.operators.relational.InExpression; import net.sf.jsqlparser.expression.operators.relational.ItemsList; import net.sf.jsqlparser.schema.Column; import org.apache.commons.collections.CollectionUtils; import org.aspectj.lang.JoinPoint; import org.aspectj.lang.Signature; import org.aspectj.lang.annotation.After; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; import org.aspectj.lang.annotation.Pointcut; import org.aspectj.lang.reflect.MethodSignature; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import java.lang.reflect.Method; import java.util.List; import java.util.Map; import java.util.stream.Collectors; @Aspect @Slf4j @Component public class WarehouseFilterConfig implements DataPermissionHandler { /** * 通过ThreadLocal记录权限相关的属性值 */ ThreadLocal<DataScopeParam> threadLocal = new ThreadLocal<>(); @Autowired private WarehousePermissionUtils userUtils; /** * 清空当前线程上次保存的权限信息 */ @After("dataScopePointCut()") public void clearThreadLocal(){ threadLocal.remove(); } /** * 注解对象 * 该成员变量在并发情况下导致多个线程公用了一套 @DataScope 配置的参数,导致sql拼接失败,必须改为局部变量 * 修改于:2022.04.28 */ // private DataScope controllerDataScope; /** * 配置织入点 */ @Pointcut("@annotation(com.sw.mes.core.annotation.WarehouseDataScope)") public void dataScopePointCut() { } @Before("dataScopePointCut()") public void doBefore(JoinPoint point) { // 获得注解 WarehouseDataScope controllerDataScope = getAnnotationLog(point); if (controllerDataScope != null) { // 获取当前的用户及相关属性,需提前获取和保存数据权限对应的部门ID集合 Map<String, List<Integer>> permission = userUtils.getPermission(); DataScopeParam dataScopeParam = new DataScopeParam(controllerDataScope.tableAlias(), controllerDataScope.warehouseField(), BaseContextHandler.getUsername().equals("admin")?false:false, permission.get("warehouseModel")); threadLocal.set(dataScopeParam); } } /** * 是否存在注解,如果存在就获取 */ private WarehouseDataScope getAnnotationLog(JoinPoint joinPoint) { Signature signature = joinPoint.getSignature(); MethodSignature methodSignature = (MethodSignature) signature; Method method = methodSignature.getMethod(); if (method != null) { return method.getAnnotation(WarehouseDataScope.class); } return null; } /** * @param where 原SQL Where 条件表达式 * @param mappedStatementId Mapper接口方法ID * @return */ @SneakyThrows @Override public Expression getSqlSegment(Expression where, String mappedStatementId) { DataScopeParam dataScopeParam = threadLocal.get(); if(dataScopeParam == null || dataScopeParam.isAdmin()){ return where; } if (where == null) { where = new HexValue(" 1 = 1 "); } String deptSql = "".equals(dataScopeParam.tableAlias) ? dataScopeParam.warehouseField : dataScopeParam.tableAlias + "." + dataScopeParam.warehouseField; // 把集合转变为JSQLParser需要的元素列表 ItemsList itemsList; if(CollectionUtils.isEmpty(dataScopeParam.secretary)){ //如果权限为空,则只能看自己部门的 itemsList = null; }else { //查看权限内的数据 itemsList = new ExpressionList(dataScopeParam.secretary.stream().map(LongValue::new).collect(Collectors.toList())); } InExpression inExpression = new InExpression(new Column(deptSql), itemsList); return new AndExpression(where, inExpression); } /** * ThreadLocal存储对象 */ @Data @AllArgsConstructor static class DataScopeParam{ /** * 作用表别名 */ private String tableAlias; /** * 仓库作用字段名 */ private String warehouseField; /** * 是否是管理员 */ private boolean isAdmin; /** * 数据权限范围 */ private List<Integer> secretary; } }
- 把自定义好的类注入到MyBatisCofig配置类中,这样才能保障每次需要执行SQL时都会走自定义的类
@Configuration @MapperScan("com.sw.mes.store.mapper") public class MybatisConfig { @Autowired @Lazy private WarehouseFilterConfig warehouseFilterConfig; /** * 数据库驱动类 */ @Value("${jdbc.driverClassName}") private String driverClassName; /** * 数据库路径测试环境 */ @Value("${jdbc.url}") private String url; /** * 数据库名称 */ @Value("${jdbc.username}") private String username; /** * 数据库密码 */ @Value("${jdbc.password}") private String password; /** * 默认提交 */ private static final boolean defaultAutoCommit = true; /** * 连接池的最大数据库连接数。设为0表示无限制。 */ private static final int maxActive = 150; /** * 最大空闲数,数据库连接的最大空闲时间。超过空闲时间,数据库连接将被标记为不可用,然后被释放。设为0表示无限制。 */ private static final int maxIdle = 100; /** * 最小空闲数, */ private static final int minIdle = 5; /** * 最大建立连接等待时间。如果超过此时间将接到异常。设为-1表示无限制。 */ private static final int maxWait = 60000; @Bean public DataSource getDataSource() { DruidDataSource dataSource = new DruidDataSource(); dataSource.setDriverClassName(driverClassName); dataSource.setUrl(url); dataSource.setUsername(username); dataSource.setPassword(password); dataSource.setDefaultAutoCommit(defaultAutoCommit); dataSource.setMaxActive(maxActive); dataSource.setMinIdle(minIdle); dataSource.setMaxWait(maxWait); dataSource.setTestWhileIdle(true); dataSource.setValidationQuery("select 1"); dataSource.setTestOnBorrow(true); dataSource.setTestOnReturn(false); dataSource.setRemoveAbandoned(true); dataSource.setRemoveAbandonedTimeout(120); dataSource.setTimeBetweenEvictionRunsMillis(30000); dataSource.setMinEvictableIdleTimeMillis(1800000); return dataSource; } @Bean public MybatisSqlSessionFactoryBean getSqlSessionFactory(DataSource dataSource) throws Exception { MybatisSqlSessionFactoryBean mybatisSqlSessionFactoryBean = new MybatisSqlSessionFactoryBean(); mybatisSqlSessionFactoryBean.setDataSource(dataSource); mybatisSqlSessionFactoryBean.setTypeAliasesPackage("com.sw.mes.store.entity"); PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver(); mybatisSqlSessionFactoryBean.setConfigLocation(resolver.getResource("classpath:config/mybatis-config.xml")); mybatisSqlSessionFactoryBean.setMapperLocations(resolver.getResources("classpath*:mapper/*.xml")); MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); // 添加数据权限插件 DataPermissionInterceptor warehousePermission = new DataPermissionInterceptor(); // 添加自定义的数据权限处理器 warehousePermission.setDataPermissionHandler(warehouseFilterConfig); interceptor.addInnerInterceptor(warehousePermission); //设置分页插件 interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); Interceptor[] plugins = {interceptor}; mybatisSqlSessionFactoryBean.setPlugins(plugins); return mybatisSqlSessionFactoryBean; } @Bean public PlatformTransactionManager transactionManager(DataSource dataSource) { return new DataSourceTransactionManager(dataSource); } }
- 注解使用说明:
该注解使用的对象实在具体的方法上,如查询分页列表接口可以直接在上面加上注解,注解中有两个参数,一个是作用表的别名,一个是作用字段,目前设计的是字段默认是warehouse_id,如果有变化需要手动在注解后把第二个参数填入@WarehouseDataScope:仓库数据权限注解,该方法需要按照仓库模式数据权限过滤则加上该注解,否则反之
-- tableAlias: 作用表别名参数 必填
-- warehouseField: 作用字段参数 选填,不填则填入默认值
总结:思路永远都是引路者,不是实现者,具体实现还是要根据自己的实际业务做。嗯~在码农的路上保持分享!!!
JPA的数据权限也做了,但是做的不是很完美,后面完善之后在分享吧!