前端的菜单和按钮权限都可以通过配置来实现,但很多时候,后台查询数据库数据的权限需要通过手动添加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. 聊天小精灵ChatGPT,好与不好大揭秘!

    一.引言 在一个遥远的地球上,有一个名为ChatGPT的魔法盒子,它能够用智慧回答你的问题,解决你的困扰.它是一个聪明的家伙,但和任何家伙一样,有优点也有缺点.现在就让我们一起来探索这个神秘的魔法盒子 ...

  2. 使用Docusaurus搭建个人网站

    第一次使用 Docusaurus 搭建我的个人网站 第一步 安装 node 环境 安装 Node.js 16.14 或更高版本(可以通过执行 node -v 命令来查看当前所用的 Node.js 版本 ...

  3. TypeScript 学习总结

    TypeScript JavaScript 语言 面向对象编程语言 面向脚本编程 是否支持可选参数 支持 不支持 是否支持静态类型 支持 不支持 是否支持接口 支持 不支持 TS:是JS的超集,即对J ...

  4. kubernetes核心实战(五)--- StatefulSets

    7.StatefulSets StatefulSet 是用来管理有状态应用的工作负载 API 对象. StatefulSet 用来管理 Deployment 和扩展一组 Pod,并且能为这些 Pod ...

  5. python之pdf转换操作 PyMuPDF库学习

    1. 资料链接github地址: pymupdf/PyMuPDF: Python bindings for MuPDF's rendering library官方手册: PyMuPDF Documen ...

  6. arc076f F - Exhausted?

    ARC076 F - Exhausted? [题目大意] \(有m个座位,分别位于坐标为1,2,3,...,m的地方:n个客人,第i位客人只坐位于[0,li]∪[ri,m]的座位.每个座位只能坐一个人 ...

  7. kali linux 基本渗透测试流程

    渗透测试流程 1. 信息收集阶段 网络拓扑结构分析 使用nmap扫描目标网络,获取目标主机IP地址和开放端口信息 使用whois查询目标域名的注册信息和DNS服务器信息 使用nslookup查询目标域 ...

  8. python:字典中遍历key对应的value值

    问题描述:python在一个字典中想要遍历某个可以对应的value值. 效果如下: 方法一:使用循环遍历所有的key,然后对比输入的值跟便利出来的key有没有相同的 constellation = [ ...

  9. 【数据结构与算法】无向图的结构与遍历 BFS&DFS

    1 表示无向图的数据类型 1.1 邻接矩阵 可以使用一个V*V的二维布尔矩阵,当定点v和定点w相连的时候,定义第v行第w列的值为true,否则为false.邻接矩阵不适合定点较多的情况,含有百万的顶点 ...

  10. 命令行编译和执行java代码

    虽然现在IDE很强大又很智能,但是平常随意写点练手的代码的时候,直接在命令行中使用vim和java命令更为方便快捷,可以做到无鼠标纯键盘的操作. 首先保证将java相关指令添加到了环境变量中: 1.编 ...