提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
前言
工作中,mybatis的${}如果能切实避免sql注入,还是很好用的,比如动态字段、动态表名,但代码安全扫描可不管能不能避免,见到${}直接判定高风险,甚是恼人啊。我这几天就遇到了,动态表名功能可以用mybatis-plus里的DynamicTableNameParser类实现,但我用的是mybatis,框架不宜轻易更换,没办法只能研究DynamicTableNameParser,模仿着自己写一个动态表名的功能,然后赌气式的自定义了一个@{}替换符,我就不信你安全扫描还能扫出@{}
提示:以下是本篇文章正文内容,下面案例可供参考
一、研究一下DynamicTableNameParser的原理
1.mybatis的plugin功能
mybatis的plugin是针对于mybatis四大组件(Statementhandler、resultsethandler、parameterHandler、executor)做增强操作的,相当于spring中的AOP,平时用的最多的分页插件PageHelper就是通过plugin实现的,如以下mybatis-plus自带的分页插件代码
@Setter
@Deprecated
@Accessors(chain = true)
@Intercepts({@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})})
public class PaginationInterceptor extends AbstractSqlParserHandler implements Interceptor {
实现Interceptor 接口,加上@Intercepts注解即可实现plugin功能,@Signature配置拦截规则,可配置多个,type只能配四大上述四大对象中的一个,配其他无效,method配对应的对象里的方法,args配方法的所有参数(不要多配或者遗漏,否则报错,找不到拦截的方法)。当程序执行到StatementHandler的prepare方法时,就会被拦截,执行PaginationInterceptor的intercept方法
@Override
public Object intercept(Invocation invocation) throws Throwable {
StatementHandler statementHandler = PluginUtils.realTarget(invocation.getTarget());
MetaObject metaObject = SystemMetaObject.forObject(statementHandler);
// SQL 解析
this.sqlParser(metaObject);
intercept方法的其他部分未粘贴进来,感兴趣的可以看下源码,继续跟sqlParser方法,调用AbstractSqlParserHandler里的sqlParser方法
public abstract class AbstractSqlParserHandler {
//在应用程序里,传入DynamicTableNameParser
private List<ISqlParser> sqlParserList;
private ISqlParserFilter sqlParserFilter;
/**
* 拦截 SQL 解析执行
*/
protected void sqlParser(MetaObject metaObject) {
if (null != metaObject) {
Object originalObject = metaObject.getOriginalObject();
StatementHandler statementHandler = PluginUtils.realTarget(originalObject);
metaObject = SystemMetaObject.forObject(statementHandler);
if (null != this.sqlParserFilter && this.sqlParserFilter.doFilter(metaObject)) {
return;
}
// @SqlParser(filter = true) 跳过该方法解析
if (SqlParserHelper.getSqlParserInfo(metaObject)) {
return;
}
// SQL 解析
if (CollectionUtils.isNotEmpty(this.sqlParserList)) {
// 好像不用判断也行,为了保险起见,还是加上吧.
statementHandler = metaObject.hasGetter("delegate") ? (StatementHandler) metaObject.getValue("delegate") : statementHandler;
if (!(statementHandler instanceof CallableStatementHandler)) {
// 标记是否修改过 SQL
boolean sqlChangedFlag = false;
String originalSql = (String) metaObject.getValue(PluginUtils.DELEGATE_BOUNDSQL_SQL);
for (ISqlParser sqlParser : this.sqlParserList) {
//使用DynamicTableNameParser
if (sqlParser.doFilter(metaObject, originalSql)) {
SqlInfo sqlInfo = sqlParser.parser(metaObject, originalSql);
if (null != sqlInfo) {
originalSql = sqlInfo.getSql();
sqlChangedFlag = true;
}
}
}
if (sqlChangedFlag) {
metaObject.setValue(PluginUtils.DELEGATE_BOUNDSQL_SQL, originalSql);
}
}
}
}
}
}
重点看这里 for (ISqlParser sqlParser : this.sqlParserList) {,sqlParserList里装的是一系列的实现了ISqlParser接口的解析器,SqlInfo sqlInfo = sqlParser.parser(metaObject, originalSql);这一行实现了sql里表名的替换
public class DynamicTableNameParser implements ISqlParser {
//String为需要替换的表名,ITableNameHandler为动态表名处理器,即替换规则
private Map<String, ITableNameHandler> tableNameHandlerMap;
@Override
public SqlInfo parser(MetaObject metaObject, String sql) {
Assert.isFalse(CollectionUtils.isEmpty(tableNameHandlerMap), "tableNameHandlerMap is empty.");
if (allowProcess(metaObject)) {
//将sql语句按空格切开成一个一个单词,然后通过from join等关键字确定表名
Collection<String> tables = new TableNameParser(sql).tables();
if (CollectionUtils.isNotEmpty(tables)) {
boolean sqlParsed = false;
String parsedSql = sql;
for (final String table : tables) {
ITableNameHandler tableNameHandler = tableNameHandlerMap.get(table);
if (null != tableNameHandler) {
//找到了该表名的替换器,process方法里替换
parsedSql = tableNameHandler.process(metaObject, parsedSql, table);
sqlParsed = true;
}
}
if (sqlParsed) {
return SqlInfo.newInstance().setSql(parsedSql);
}
}
}
return null;
}
public boolean allowProcess(MetaObject metaObject) {
return true;
}
}
感兴趣的朋友可以网上搜一下mybaits-plus实现动态表名的方法
二、实现自己的插件和sql解析器
按照mybatis-plus的步骤指引,一步步实现即可,先定义plugin,然后定义sql解析器,把mybatis-plus能用的类copy过来,不报错就差不多了。接下来引入springboot,将plugin连带着sql解析器注入进spring容器,建mapper即可验证,上代码,gitee地址:https://gitee.com/wangchaoam/mybatisCustomLabel.git
总结
因看不惯银行对${}的一刀切,搞出了@{}来代替 ${},其实最简单的方法还是改mybatis的源码,不用写这么多代码,不用花时间搞懂mybatis-plus里大佬写的代码,只要将@{}加到解析器里,地位等同于 ${}即可,但maven库又被锁死,休想上传自己的jar包。如有问题,欢迎提出,人生中第一篇博文,分享一下这两天的成功