本文首发在我的博客:https://blog.liuzijian.com/post/mybatis-plus-source-data-permission-interceptor.html

一、概述

DataPermissionInterceptor是MyBatis-Plus中的一个拦截器插件,用于实现数据权限功能,它将查询、删除和修改的SQL进行拦截并获得要执行的SQL,并解析出SQL中的表和原有条件,通过一个DataPermissionHandler接口来回调获取每个表的数据权限条件,再和原有的条件拼接在一起形成新的SQL,执行重写后的新SQL,从而实现数据权限功能。因为添加操作无需数据权限控制,因此不处理添加的情况。

本类的实现较为简单,因为对于数据权限来说,对于比较复杂的查询SQL的解析逻辑基本已经由父类完成,具体见:BaseMultiTableInnerInterceptor源码解读,本类作为子类将查询SQL调用父类进行解析重写即可,对于删除和更新的SQL仅仅针对delete和update本身的where条件进行处理,而且是单表操作,因此对于删除和更新来说,只是将表原有条件和数据权限条件做简单的拼接即可。

本文基于MyBatis-Plus的3.5.9版本的源码,并fork了代码: https://github.com/changelzj/mybatis-plus/tree/lzj-3.5.9

public class DataPermissionInterceptor extends BaseMultiTableInnerInterceptor implements InnerInterceptor {

    private DataPermissionHandler dataPermissionHandler;

    @SuppressWarnings("RedundantThrows")
@Override
public void beforeQuery(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {...} @Override
public void beforePrepare(StatementHandler sh, Connection connection, Integer transactionTimeout) {...} @Override
protected void processSelect(Select select, int index, String sql, Object obj) {...} protected void setWhere(PlainSelect plainSelect, String whereSegment) {...} @Override
protected void processUpdate(Update update, int index, String sql, Object obj) {...} @Override
protected void processDelete(Delete delete, int index, String sql, Object obj) {...} protected Expression getUpdateOrDeleteExpression(final Table table, final Expression where, final String whereSegment) {...} @Override
public Expression buildTableExpression(final Table table, final Expression where, final String whereSegment) {...}
}

二、源码解读

2.1 beforeQuery

该方法从InnerInterceptor接口继承而来,是解析查询SQL的起点,MyBatis-Plus执行时就是对实现InnerInterceptor接口的类中的对应方法进行回调的,会传入要执行的SQL并接收重写后的SQL来实现对SQL的修改,在查询SQL执行前进行拦截并调用beforeQuery()beforeQuery()中再去调用parserSingle()

parserSingle()是从父类BaseMultiTableInnerInterceptor自JsqlParserSupport抽象类间接继承而来的,JsqlParserSupport类的功能非常简单,作用是判断SQL是增删改查的哪一种类型,然后分别调用对应的方法开始解析。

当调用parserSingle()并传入SQL时,会在JsqlParserSupport的processParser()方法中先判断是哪一种Statement,然后分别强转为具体的Select、Update、Delete、Insert对象,再调用该类间接继承并重写的processSelect()方法并传入Select对象。

processSelect()方法会再调用父类的processSelectBody()对查询SQL进行解析,对于解析到的每张表和已有条件,再去调用父类的builderExpression()进而再调用buildTableExpression()获取当前表对应的数据权限过滤条件再和已有条件进行拼接。

@SuppressWarnings("RedundantThrows")
@Override
public void beforeQuery(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
if (InterceptorIgnoreHelper.willIgnoreDataPermission(ms.getId())) {
return;
}
PluginUtils.MPBoundSql mpBs = PluginUtils.mpBoundSql(boundSql);
mpBs.sql(parserSingle(mpBs.sql(), ms.getId()));
}

2.2 beforePrepare

该方法和beforeQuery()一样,也是从InnerInterceptor接口中继承而来,因为添加修改和删除SQL都要预编译,因此该方法可作为解析删除和修改SQL的起点,不同的是beforePrepare()调用的是JsqlParserSupport中继承来的parserMulti(),因为查询语句只能一次执行一条,但是增删改语句可以用分号间隔一次执行多条,故需调用parserMulti()将多个语句循环拆开,然后判断并分别强转为具体的Select、Update、Delete、Insert对象,再分别调用该类间接继承并重写的processDelete()processUpdate()方法并分别传入Delete,Update对象,然后直接解析出要删除和更新数据的表和已有删除更新条件,调用父类的andExpression()进而在调用buildTableExpression()来拼接数据权限过滤条件。

@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.UPDATE || sct == SqlCommandType.DELETE) {
if (InterceptorIgnoreHelper.willIgnoreDataPermission(ms.getId())) {
return;
}
PluginUtils.MPBoundSql mpBs = mpSh.mPBoundSql();
mpBs.sql(parserMulti(mpBs.sql(), ms.getId()));
}
}

2.3 processSelect

开始一个对查询SQL的解析,当前版本走的是if (dataPermissionHandler instanceof MultiDataPermissionHandler)的新版本的逻辑,先调用processSelectBody()进行解析,对于WITH中的结构,又在调用processSelectBody()后单独组织了一段针对WITH中的查询的解析逻辑。旧版本应该是直接获取where后面的条件直接传递给dataPermissionHandler,在dataPermissionHandler中对where进行追加,而新版本代码是将解析到的表传到dataPermissionHandler,传入的是表名返回表的数据权限条件

@Override
protected void processSelect(Select select, int index, String sql, Object obj) {
if (dataPermissionHandler == null) {
return;
}
if (dataPermissionHandler instanceof MultiDataPermissionHandler) {
// 参照 com.baomidou.mybatisplus.extension.plugins.inner.TenantLineInnerInterceptor.processSelect 做的修改
final String whereSegment = (String) obj;
processSelectBody(select, whereSegment);
List<WithItem> withItemsList = select.getWithItemsList();
if (!CollectionUtils.isEmpty(withItemsList)) {
withItemsList.forEach(withItem -> processSelectBody(withItem, whereSegment));
}
} else {
// 兼容原来的旧版 DataPermissionHandler 场景
if (select instanceof PlainSelect) {
this.setWhere((PlainSelect) select, (String) obj);
} else if (select instanceof SetOperationList) {
SetOperationList setOperationList = (SetOperationList) select;
List<Select> selectBodyList = setOperationList.getSelects();
selectBodyList.forEach(s -> this.setWhere((PlainSelect) s, (String) obj));
}
}
}

2.4 setWhere

这段代码应该是为旧版本用的,没有走到

/**
* 设置 where 条件
*
* @param plainSelect 查询对象
* @param whereSegment 查询条件片段
*/
protected void setWhere(PlainSelect plainSelect, String whereSegment) {
if (dataPermissionHandler == null) {
return;
}
// 兼容旧版的数据权限处理
final Expression sqlSegment = dataPermissionHandler.getSqlSegment(plainSelect.getWhere(), whereSegment);
if (null != sqlSegment) {
plainSelect.setWhere(sqlSegment);
}
}

2.5 processUpdate

/**
* update 语句处理
*/
@Override
protected void processUpdate(Update update, int index, String sql, Object obj) {
final Expression sqlSegment = getUpdateOrDeleteExpression(update.getTable(), update.getWhere(), (String) obj);
if (null != sqlSegment) {
update.setWhere(sqlSegment);
}
}

2.6 processDelete

/**
* delete 语句处理
*/
@Override
protected void processDelete(Delete delete, int index, String sql, Object obj) {
final Expression sqlSegment = getUpdateOrDeleteExpression(delete.getTable(), delete.getWhere(), (String) obj);
if (null != sqlSegment) {
delete.setWhere(sqlSegment);
}
}

2.7 getUpdateOrDeleteExpression

针对更新和删除的SQL,不同于查询,当更新后的值是子查询或更新删除条件的值是一个子查询的时候,不会为这个子查询中的表追加条件,仅把针对整个update或delete语句的条件本身和要追加的数据权限过滤条件进行AND和OR拼接,因此会直接把表名和WHERE条件调用父类的andExpression(table, where, whereSegment)进行拼接,方法的返回值即为拼接后的结果,直接返回。

protected Expression getUpdateOrDeleteExpression(final Table table, final Expression where, final String whereSegment) {
if (dataPermissionHandler == null) {
return null;
}
if (dataPermissionHandler instanceof MultiDataPermissionHandler) {
return andExpression(table, where, whereSegment);
} else {
// 兼容旧版的数据权限处理
return dataPermissionHandler.getSqlSegment(where, whereSegment);
}
}

2.8 buildTableExpression

传入表名,返回表要追加的数据权限过滤条件,具体哪个表需要怎样的数据权限条件,会通过回调dataPermissionHandler.getSqlSegment()让DataPermissionHandler的实现类根据具体业务来确定

@Override
public Expression buildTableExpression(final Table table, final Expression where, final String whereSegment) {
if (dataPermissionHandler == null) {
return null;
}
// 只有新版数据权限处理器才会执行到这里
final MultiDataPermissionHandler handler = (MultiDataPermissionHandler) dataPermissionHandler;
return handler.getSqlSegment(table, where, whereSegment);
}

DataPermissionInterceptor源码解读的更多相关文章

  1. SDWebImage源码解读之SDWebImageDownloaderOperation

    第七篇 前言 本篇文章主要讲解下载操作的相关知识,SDWebImageDownloaderOperation的主要任务是把一张图片从服务器下载到内存中.下载数据并不难,如何对下载这一系列的任务进行设计 ...

  2. SDWebImage源码解读 之 NSData+ImageContentType

    第一篇 前言 从今天开始,我将开启一段源码解读的旅途了.在这里先暂时不透露具体解读的源码到底是哪些?因为也可能随着解读的进行会更改计划.但能够肯定的是,这一系列之中肯定会有Swift版本的代码. 说说 ...

  3. SDWebImage源码解读 之 UIImage+GIF

    第二篇 前言 本篇是和GIF相关的一个UIImage的分类.主要提供了三个方法: + (UIImage *)sd_animatedGIFNamed:(NSString *)name ----- 根据名 ...

  4. SDWebImage源码解读 之 SDWebImageCompat

    第三篇 前言 本篇主要解读SDWebImage的配置文件.正如compat的定义,该配置文件主要是兼容Apple的其他设备.也许我们真实的开发平台只有一个,但考虑各个平台的兼容性,对于框架有着很重要的 ...

  5. SDWebImage源码解读_之SDWebImageDecoder

    第四篇 前言 首先,我们要弄明白一个问题? 为什么要对UIImage进行解码呢?难道不能直接使用吗? 其实不解码也是可以使用的,假如说我们通过imageNamed:来加载image,系统默认会在主线程 ...

  6. SDWebImage源码解读之SDWebImageCache(上)

    第五篇 前言 本篇主要讲解图片缓存类的知识,虽然只涉及了图片方面的缓存的设计,但思想同样适用于别的方面的设计.在架构上来说,缓存算是存储设计的一部分.我们把各种不同的存储内容按照功能进行切割后,图片缓 ...

  7. SDWebImage源码解读之SDWebImageCache(下)

    第六篇 前言 我们在SDWebImageCache(上)中了解了这个缓存类大概的功能是什么?那么接下来就要看看这些功能是如何实现的? 再次强调,不管是图片的缓存还是其他各种不同形式的缓存,在原理上都极 ...

  8. AFNetworking 3.0 源码解读 总结(干货)(下)

    承接上一篇AFNetworking 3.0 源码解读 总结(干货)(上) 21.网络服务类型NSURLRequestNetworkServiceType 示例代码: typedef NS_ENUM(N ...

  9. AFNetworking 3.0 源码解读 总结(干货)(上)

    养成记笔记的习惯,对于一个软件工程师来说,我觉得很重要.记得在知乎上看到过一个问题,说是人类最大的缺点是什么?我个人觉得记忆算是一个缺点.它就像时间一样,会自己消散. 前言 终于写完了 AFNetwo ...

  10. AFNetworking 3.0 源码解读(十一)之 UIButton/UIProgressView/UIWebView + AFNetworking

    AFNetworking的源码解读马上就结束了,这一篇应该算是倒数第二篇,下一篇会是对AFNetworking中的技术点进行总结. 前言 上一篇我们总结了 UIActivityIndicatorVie ...

随机推荐

  1. 利用Linq Skip() Take()分页

    private void TestPostData() { string all = ""; List<int> listTimeCard = new List< ...

  2. Spring Cloud Alibaba实战,从微服务架构到基本服务配置

    https://blog.csdn.net/itcast_cn/article/details/124558887 Spring Cloud Alibaba 实战 1目标理解什么是微服务架构理解什么是 ...

  3. RPC框架的实现原理,及RPC架构组件详解

    RPC的由来 随着互联网的发展,网站应用的规模不断扩大,常规的垂直应用架构已无法应对,分布式服务架构以及流动计算架构势在必行,亟需一个治理系统确保架构有条不紊的演进. 单一应用架构 当网站流量很小时, ...

  4. moectf2023 web wp

    gas!gas!gas! 直接跑脚本 import requests session=requests.Session() url="http://127.0.0.1:14447" ...

  5. Win10安装MySql步骤

    1.下载 下载地址:https://dev.mysql.com/downloads/mysql/ 文件地址:https://dev.mysql.com/get/Downloads/MySQL-8.3/ ...

  6. pip安装MySQLdb报错mysql_config not found

    报错EnvironmentError: mysql_config not found解决方法 1.sudo apt-get install python-setuptools 2.sudo apt-g ...

  7. NET中三种主机简单理解

    在NET中有三个不同的主机: .NET WebApplication 主机,也称为最小主机. 这是.NET 6中的一个新特性,旨在提供最小的启动时间和内存消耗.最小主机只包括.NET运行时的最基本组件 ...

  8. Bean的原始版本与最终版本不一致?记一次Spring IOC探索之旅

    前言 在这个信息技术发展迅速的时代,万万没想到,Spring自2003年发展至今,仍是技术选型中的首选,某些项目甚至有Spring全家桶的情况. 在Java开发者面试当中,Spring的原理也常被面试 ...

  9. 【.NET】调用本地 Deepseek 模型

    本篇咱们来聊一聊怎么在 .NET 代码中使用本地部署的 Deepseek 语言模型.大伙伴们不必要紧张,很简单的,你不需要学习新知识,只要你知道 .NET 如何访问 HTTP 和 JSON 的序列化相 ...

  10. 【由技及道】SpringBoot启动即退出的量子纠缠诊断实录【人工智障AI2077的开发问题日志】

    问题分析:这个Spring Boot怕不是属蜉蝣的? (人工智障OS:主人在容器环境部署的Spring应用生命周期堪比蜉蝣成虫--朝生暮死,启动即消亡) 现象观察: /usr/lib/jvm/jdk- ...