前端的菜单和按钮权限都可以通过配置来实现,但很多时候,后台查询数据库数据的权限需要通过手动添加SQL来实现。

比如员工打卡记录表,有id,name,dpt_id,company_id等字段,后两个表示部门ID和分公司ID。

查看员工打卡记录SQL为:select id,name,dpt_id,company_id from t_record

当一个总部账号可以查看全部数据此时,sql无需改变。因为他可以看到全部数据。

当一个部门管理员权限员工查看全部数据时,sql需要在末属添加 where dpt_id = #{dpt_id}

如果每个功能模块都需要手动写代码去拿到当前登陆用户的所属部门,然后手动添加where条件,就显得非常的繁琐。

因此,可以通过mybatis的拦截器拿到查询sql语句,再自动改写sql。

mybatis 拦截器

MyBatis 允许你在映射语句执行过程中的某一点进行拦截调用。默认情况下,MyBatis 允许使用插件来拦截的方法调用包括:

  • Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)
  • ParameterHandler (getParameterObject, setParameters)
  • ResultSetHandler (handleResultSets, handleOutputParameters)
  • StatementHandler (prepare, parameterize, batch, update, query)

这些类中方法的细节可以通过查看每个方法的签名来发现,或者直接查看 MyBatis 发行包中的源代码。 如果你想做的不仅仅是监控方法的调用,那么你最好相当了解要重写的方法的行为。 因为在试图修改或重写已有方法的行为时,很可能会破坏 MyBatis 的核心模块。 这些都是更底层的类和方法,所以使用插件的时候要特别当心。

通过 MyBatis 提供的强大机制,使用插件是非常简单的,只需实现 Interceptor 接口,并指定想要拦截的方法签名即可。

分页插件pagehelper就是一个典型的通过拦截器去改写SQL的。

可以看到它通过注解 @Intercepts 和签名 @Signature 来实现,拦截Executor执行器,拦截所有的query查询类方法。

我们可以据此也实现自己的拦截器。

点击查看代码
import com.skycomm.common.util.user.Cpip2UserDeptVo;
import com.skycomm.common.util.user.Cpip2UserDeptVoUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.ibatis.cache.CacheKey;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.SqlSource;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.plugin.Intercepts;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Signature;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes; import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method; @Component
@Intercepts({
@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}),
@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class}),
})
@Slf4j
public class MySqlInterceptor implements Interceptor { @Override
public Object intercept(Invocation invocation) throws Throwable {
MappedStatement statement = (MappedStatement) invocation.getArgs()[0];
Object parameter = invocation.getArgs()[1];
BoundSql boundSql = statement.getBoundSql(parameter);
String originalSql = boundSql.getSql();
Object parameterObject = boundSql.getParameterObject(); SqlLimit sqlLimit = isLimit(statement);
if (sqlLimit == null) {
return invocation.proceed();
} RequestAttributes req = RequestContextHolder.getRequestAttributes();
if (req == null) {
return invocation.proceed();
} //处理request
HttpServletRequest request = ((ServletRequestAttributes) req).getRequest();
Cpip2UserDeptVo userVo = Cpip2UserDeptVoUtil.getUserDeptInfo(request);
String depId = userVo.getDeptId(); String sql = addTenantCondition(originalSql, depId, sqlLimit.alis());
log.info("原SQL:{}, 数据权限替换后的SQL:{}", originalSql, sql);
BoundSql newBoundSql = new BoundSql(statement.getConfiguration(), sql, boundSql.getParameterMappings(), parameterObject);
MappedStatement newStatement = copyFromMappedStatement(statement, new BoundSqlSqlSource(newBoundSql));
invocation.getArgs()[0] = newStatement;
return invocation.proceed();
} /**
* 重新拼接SQL
*/
private String addTenantCondition(String originalSql, String depId, String alias) {
String field = "dpt_id";
if(StringUtils.isNoneBlank(alias)){
field = alias + "." + field;
} StringBuilder sb = new StringBuilder(originalSql);
int index = sb.indexOf("where");
if (index < 0) {
sb.append(" where ") .append(field).append(" = ").append(depId);
} else {
sb.insert(index + 5, " " + field +" = " + depId + " and ");
}
return sb.toString();
} private MappedStatement copyFromMappedStatement(MappedStatement ms, SqlSource newSqlSource) {
MappedStatement.Builder builder = new MappedStatement.Builder(ms.getConfiguration(), ms.getId(), newSqlSource, ms.getSqlCommandType());
builder.resource(ms.getResource());
builder.fetchSize(ms.getFetchSize());
builder.statementType(ms.getStatementType());
builder.keyGenerator(ms.getKeyGenerator());
builder.timeout(ms.getTimeout());
builder.parameterMap(ms.getParameterMap());
builder.resultMaps(ms.getResultMaps());
builder.cache(ms.getCache());
builder.useCache(ms.isUseCache());
return builder.build();
} /**
* 通过注解判断是否需要限制数据
* @return
*/
private SqlLimit isLimit(MappedStatement mappedStatement) {
SqlLimit sqlLimit = null;
try {
String id = mappedStatement.getId();
String className = id.substring(0, id.lastIndexOf("."));
String methodName = id.substring(id.lastIndexOf(".") + 1, id.length());
final Class<?> cls = Class.forName(className);
final Method[] method = cls.getMethods();
for (Method me : method) {
if (me.getName().equals(methodName) && me.isAnnotationPresent(SqlLimit.class)) {
sqlLimit = me.getAnnotation(SqlLimit.class);
}
}
} catch (Exception e) {
e.printStackTrace();
}
return sqlLimit;
} public static class BoundSqlSqlSource implements SqlSource { private final BoundSql boundSql; public BoundSqlSqlSource(BoundSql boundSql) {
this.boundSql = boundSql;
} @Override
public BoundSql getBoundSql(Object parameterObject) {
return boundSql;
}
}
}

顺便加了个注解 @SqlLimit,在mapper方法上加了此注解才进行数据权限过滤。

同时注解有两个属性,

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface SqlLimit {
/**
* sql表别名
* @return
*/
String alis() default ""; /**
* 通过此列名进行限制
* @return
*/
String columnName() default "";
}

columnName表示通过此列名进行限制,一般来说一个系统,各表当中的此列是统一的,可以忽略。

alis用于标注sql表别名,如 针对sql select * from tablea as a left join tableb as b on a.id = b.id 进行改写,如果不知道表别名,会直接在后面拼接 where dpt_id = #{dptId},

那此SQL就会错误的,通过别名 @SqlLimit(alis = "a") 就可以知道需要拼接的是 where a.dpt_id = #{dptId}

执行结果

原SQL:select * from person, 数据权限替换后的SQL:select * from person where dpt_id = 234

原SQL:select * from person where id > 1, 数据权限替换后的SQL:select * from person where dpt_id = 234 and id > 1

但是在使用PageHelper进行分页的时候还是有问题。

可以看到先执行了_COUNT方法也就是PageHelper,再执行了自定义的拦截器。

在我们的业务方法中注入SqlSessionFactory

@Autowired
@Lazy
private List<SqlSessionFactory> sqlSessionFactoryList;

PageInterceptor为1,自定义拦截器为0,跟order相反,PageInterceptor优先级更高,所以越先执行。

mybatis拦截器优先级

@Order

通过@Order控制PageInterceptor和MySqlInterceptor可行吗?

将MySqlInterceptor的加载优先级调到最高,但测试证明依然不行。

定义3个类

@Component
@Order(2)
public class OrderTest1 { @PostConstruct
public void init(){
System.out.println(" 00000 init");
}
}
@Component
@Order(1)
public class OrderTest2 { @PostConstruct
public void init(){
System.out.println(" 00001 init");
}
}
@Component
@Order(0)
public class OrderTest3 { @PostConstruct
public void init(){
System.out.println(" 00002 init");
}
}

OrderTest1,OrderTest2,OrderTest3的优先级从低到高。

顺序预期的执行顺序应该是相反的:

00002 init
00001 init
00000 init

但事实上执行的顺序是

00000 init
00001 init
00002 init

@Order 不控制实例化顺序,只控制执行顺序。

@Order 只跟特定一些注解生效 如:@Compent @Service @Aspect … 不生效的如: @WebFilter

所以这里达不到预期效果。

@Priority 类似,同样不行。

@DependsOn

使用此注解将当前类将在依赖类实例化之后再执行实例化。

在MySqlInterceptor上标记@DependsOn("queryInterceptor")

启动报错,

这个时候queryInterceptor还没有实例化对象。

@PostConstruct

@PostConstruct修饰的方法会在服务器加载Servlet的时候运行,并且只会被服务器执行一次。

在同一个类里,执行顺序为顺序如下:Constructor > @Autowired > @PostConstruct。

但它也不能保证不同类的执行顺序。

PageHelper的springboot start也是通过这个来初始化拦截器的。

ApplicationRunner

在当前springboot容器加载完成后执行,那么这个时候pagehelper的拦截器已经加入,在这个时候加入自定义拦截器,就能达到我们想要的效果。

仿照PageHelper来写

@Component
public class InterceptRunner implements ApplicationRunner { @Autowired
private List<SqlSessionFactory> sqlSessionFactoryList; @Override
public void run(ApplicationArguments args) throws Exception {
MySqlInterceptor mybatisInterceptor = new MySqlInterceptor();
for (SqlSessionFactory sqlSessionFactory : sqlSessionFactoryList) {
org.apache.ibatis.session.Configuration configuration = sqlSessionFactory.getConfiguration();
configuration.addInterceptor(mybatisInterceptor);
}
}
}

再执行,可以看到自定义拦截器在拦截器链当中下标变为了1(优先级与order刚好相反)

后台打印结果,达到了预期效果。

mybatis拦截器实现数据权限的更多相关文章

  1. mybatis拦截器实现通用权限字段添加

    实现效果 日常sql中直接使用权限字段实现权限内数据筛选,无需入参,直接使用,使用形式为:select * from crh_snp.channelinfo where short_code in ( ...

  2. 数据权限管理中心 - 基于mybatis拦截器实现

    数据权限管理中心 由于公司大部分项目都是使用mybatis,也是使用mybatis的拦截器进行分页处理,所以技术上也直接选择从拦截器入手 需求场景 第一种场景:行级数据处理 原sql: select ...

  3. Mybatis拦截器,修改Date类型数据。设置毫秒为0

    1:背景 Mysql自动将datetime类型的毫秒数四舍五入,比如代码中传入的Date类型的数据值为  2021.03.31 23:59:59.700     到数据库   2021.04.01 0 ...

  4. 【Java EE 学习 75 下】【数据采集系统第七天】【二进制运算实现权限管理】【使用反射初始化权限表】【权限捕获拦截器动态添加权限】

    一.使用反射动态添加权限 在该系统中,我使用struts2的时候非常规范,访问的Action的形式都是"ActionClassName_MethodName.action?参数列表" ...

  5. Mybatis拦截器 mysql load data local 内存流处理

    Mybatis 拦截器不做解释了,用过的基本都知道,这里用load data local主要是应对大批量数据的处理,提高性能,也支持事务回滚,且不影响其他的DML操作,当然这个操作不要涉及到当前所lo ...

  6. Mybatis拦截器实现分页

    本文介绍使用Mybatis拦截器,实现分页:并且在dao层,直接返回自定义的分页对象. 最终dao层结果: public interface ModelMapper { Page<Model&g ...

  7. 基于Spring和Mybatis拦截器实现数据库操作读写分离

    首先需要配置好数据库的主从同步: 上一篇文章中有写到:https://www.cnblogs.com/xuyiqing/p/10647133.html 为什么要进行读写分离呢? 通常的Web应用大多数 ...

  8. 玩转SpringBoot之整合Mybatis拦截器对数据库水平分表

    利用Mybatis拦截器对数据库水平分表 需求描述 当数据量比较多时,放在一个表中的时候会影响查询效率:或者数据的时效性只是当月有效的时候:这时我们就会涉及到数据库的分表操作了.当然,你也可以使用比较 ...

  9. Mybatis拦截器实现原理深度分析

    1.拦截器简介 拦截器可以说使我们平时开发经常用到的技术了,Spring AOP.Mybatis自定义插件原理都是基于拦截器实现的,而拦截器又是以动态代理为基础实现的,每个框架对拦截器的实现不完全相同 ...

  10. SpringMVC拦截器和数据校验

    1.什么是拦截器 Spring MVC中的拦截器(Interceptor)类似于Servlet中的过滤器(Filter),它主要用于拦截用户请求并作相应的处理.例如通过拦截器可以进行权限验证.记录请求 ...

随机推荐

  1. 自己动手从零写桌面操作系统GrapeOS系列教程——23.从硬盘读取文件

    学习操作系统原理最好的方法是自己写一个简单的操作系统. 本讲代码文件为boot.asm,要读取的文件为data.txt. 一.在FAT16系统中读取文件的流程 在GrapeOS中用到的文件少且小,所有 ...

  2. 剑指 offer 第 3 天

    第 3 天 字符串(简单) 剑指 Offer 05. 替换空格 请实现一个函数,把字符串 s 中的每个空格替换成"%20". 示例 1: 输入:s = "We are h ...

  3. 随机服务系统模拟—R实现(三)

    M/M/c随机服务系统的模拟 M/M/1服务系统:(1)队列长度没有限制:(2)顾客到达的时间间隔和服务时间均服从指数分布:(3)服务台数量为c. 一.M/M/c随机服务系统的模拟 在M/M/c排队系 ...

  4. 在k8s(kubernetes) 上安装 ingress V1.1.0

    Ingress 公开了从集群外部到集群内服务的 HTTP 和 HTTPS 路由.流量路由由 Ingress 资源上定义的规则控制. 下面是一个将所有流量都发送到同一 Service 的简单 Ingre ...

  5. [PKM] 服务器

    1 概述与基础常识 1.1 服务器的定义 定义: 服务器,英文名Server,指能提供某种服务的网络设备. 提供的主要服务包括:数据的接收和传递.数据的存储和数据的处理. 通俗点儿,我们可以把服务器比 ...

  6. Redis 数据类型 Set

    Redis 数据类型 Set(集合) Redis 常用命令,思维导图 >>> Redis 的 Set 是 String 类型的无序集合.集合成员是唯一的,这就意味着集合中不能出现重复 ...

  7. scikit-learn 中 Boston Housing 数据集问题解决方案

    scikit-learn 中 Boston Housing 数据集问题解决方案 在部分旧教程或教材中是 sklearn,现在[2023]已经变更为 scikit-learn 作用:开源机器学习库,支持 ...

  8. 【Vue项目】尚品汇(六)ShopCar组件开发 购物车模块

    4 购物车 4.1 购物车商品数量控制 Detail\index.vue <div class="cartWrap"> <div class="cont ...

  9. 以SQLserver为例的Dapper详细讲解

    Dapper是一种轻量级的ORM(对象关系映射)工具,它提供了高效且易于使用的方式来执行数据库操作.Dapper是由Stack Overflow团队开发并维护的,它的主要目标是提供比EF更快.更直接的 ...

  10. Object o = new Object();

    对象的创建过程: 1,申请内存,并初始化: 2,构造器初始化: 3,o指向对象. 对象在内存中的存储布局: 使用jol工具打印java对象在内存的存储布局: 其中,对象头的组成: 对象头包括Mark ...