千家信息网

Druid多数据源下Sql防火墙导致异常的示例分析

发表于:2025-01-31 作者:千家信息网编辑
千家信息网最后更新 2025年01月31日,本篇文章给大家分享的是有关Druid多数据源下Sql防火墙导致异常的示例分析,小编觉得挺实用的,因此分享给大家学习,希望大家阅读完这篇文章后可以有所收获,话不多说,跟着小编一起来看看吧。问题原因由于应
千家信息网最后更新 2025年01月31日Druid多数据源下Sql防火墙导致异常的示例分析

本篇文章给大家分享的是有关Druid多数据源下Sql防火墙导致异常的示例分析,小编觉得挺实用的,因此分享给大家学习,希望大家阅读完这篇文章后可以有所收获,话不多说,跟着小编一起来看看吧。

问题原因

  1. 由于应用组件同时使用了两个数据源,Oracle和Mysql,而之前一直都是Oracle,近期引入了Mysql;

  2. 应用组件生产环境配置开启了Druid的Sql防火墙配置,配置项为:spring.datasource.druid.filter.wall.enabled=true ,由于测试环境未开启,因此将问题带到了生产上面。

开启Sql防火墙后,在数据源初始化的时候会初始化一个 WallFilter .

    @Bean    @ConfigurationProperties("spring.datasource.druid.filter.wall")    @ConditionalOnProperty(        prefix = "spring.datasource.druid.filter.wall",        name = {"enabled"}    )    @ConditionalOnMissingBean    public WallFilter wallFilter(WallConfig wallConfig) {        WallFilter filter = new WallFilter();        filter.setConfig(wallConfig);        return filter;    }

问题表象

在近期版本上线后,一些跑了好几年的sql突然一夜之间报错了,守版本的弟兄遇到这种问题后,难免大惊失色,无奈之下,当晚只好回滚了版本。

下面我们来看下,已经跑了几年的sql当时报了什么错:

at com.alibaba.druid.wall.WallFilter.checkInternal(WallFilter.java:798)at com.alibaba.druid.wall.WallFilter.connection_prepareStatement(WallFilter.java:251)at com.alibaba.druid.filter.FilterChainImpl.connection_prepareStatement(FilterChainImpl.java:473)at com.alibaba.druid.filter.FilterAdapter.connection_prepareStatement(FilterAdapter.java:929)at com.alibaba.druid.filter.FilterEventAdapter.connection_prepareStatement(FilterEventAdapter.java:122)at com.alibaba.druid.filter.FilterChainImpl.connection_prepareStatement(FilterChainImpl.java:473)at com.alibaba.druid.proxy.jdbc.ConnectionProxyImpl.prepareStatement(ConnectionProxyImpl.java:342)at com.alibaba.druid.pool.DruidPooledConnection.prepareStatement(DruidPooledConnection.java:350)at org.apache.ibatis.executor.statement.PreparedStatementHandler.instantiateStatement(PreparedStatementHandler.java:87)at org.apache.ibatis.executor.statement.BaseStatementHandler.prepare(BaseStatementHandler.java:88)at org.apache.ibatis.executor.statement.RoutingStatementHandler.prepare(RoutingStatementHandler.java:59)at org.apache.ibatis.executor.SimpleExecutor.prepareStatement(SimpleExecutor.java:85)at org.apache.ibatis.executor.SimpleExecutor.doUpdate(SimpleExecutor.java:49)at org.apache.ibatis.executor.BaseExecutor.update(BaseExecutor.java:117)at org.apache.ibatis.executor.CachingExecutor.update(CachingExecutor.java:76)at org.apache.ibatis.session.defaults.DefaultSqlSession.update(DefaultSqlSession.java:198)at org.apache.ibatis.session.defaults.DefaultSqlSession.insert(DefaultSqlSession.java:185)at sun.reflect.GeneratedMethodAccessor408.invoke(Unknown Source)at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)at java.lang.reflect.Method.invoke(Method.java:498)at org.mybatis.spring.SqlSessionTemplate$SqlSessionInterceptor.invoke(SqlSessionTemplate.java:433)... 121 common frames omittedCaused by: com.alibaba.druid.sql.parser.ParserException: syntax error, error in :' USING(select ? as o', expect USING, actual IDENTIFIER pos 61, line 2, column 39, token IDENTIFIER USINGat com.alibaba.druid.sql.parser.SQLParser.printError(SQLParser.java:284)at com.alibaba.druid.sql.parser.SQLParser.accept(SQLParser.java:292)at com.alibaba.druid.sql.parser.SQLStatementParser.parseMerge(SQLStatementParser.java:2879)at com.alibaba.druid.sql.parser.SQLStatementParser.parseStatementList(SQLStatementParser.java:225)at com.alibaba.druid.sql.parser.SQLStatementParser.parseStatementList(SQLStatementParser.java:81)at com.alibaba.druid.wall.WallProvider.checkInternal(WallProvider.java:622)at com.alibaba.druid.wall.WallProvider.check(WallProvider.java:576)at com.alibaba.druid.wall.WallFilter.checkInternal(WallFilter.java:785)... 141 common frames omitted

看到上面的异常堆栈,抛出的异常的意思是:对该SQL进行解析的时候期望接受的USING关键字,但实际上在关键字库里面确没有找到USING关键字,而是把USING当成了标识符IDENTIFIER.

问题分析

打开SQLStatementParser 及 SQLParser类源码发现有以下信息:

com.alibaba.druid.sql.parser.SQLStatementParser#parseMerge

关键问题在这里,为什么关键字的词里没有找到USING关键字? 通过debug发现在第一次访问数据的时候会调用com.alibaba.druid.pool.DruidDataSource#init 方法, 该方法里有这一句:

public void init() throws SQLException {        if (inited) {            return;        }        final ReentrantLock lock = this.lock;        try {            lock.lockInterruptibly();        } catch (InterruptedException e) {            throw new SQLException("interrupt", e);        }        boolean init = false;        try {            if (inited) {                return;            }            initStackTrace = Utils.toString(Thread.currentThread().getStackTrace());            this.id = DruidDriver.createDataSourceId();            if (this.id > 1) {                long delta = (this.id - 1) * 100000;                this.connectionIdSeed.addAndGet(delta);                this.statementIdSeed.addAndGet(delta);                this.resultSetIdSeed.addAndGet(delta);                this.transactionIdSeed.addAndGet(delta);            }            if (this.jdbcUrl != null) {                this.jdbcUrl = this.jdbcUrl.trim();                initFromWrapDriverUrl();            }            // 重点在这里            for (Filter filter : filters) {                filter.init(this);            }

这句话的意思是初始化配置的Filter,其中我们启用的WallFilter就是在这里初始化的。

接下来我们进入WallFilter 的 init方法:

 @Override    public synchronized void init(DataSourceProxy dataSource) {        if (null == dataSource) {            LOG.error("dataSource should not be null");            return;        }        if (this.dbType == null || this.dbType.trim().length() == 0) {                                          // dbType 取决于第一次调用此方法的数据源的类型            if (dataSource.getDbType() != null) {                this.dbType = dataSource.getDbType();            } else {                this.dbType = JdbcUtils.getDbType(dataSource.getRawJdbcUrl(), "");            }        }        if (dbType == null) {            dbType = JdbcUtils.getDbType(dataSource.getUrl(), null);        }        if (JdbcUtils.MYSQL.equals(dbType) || //            JdbcUtils.MARIADB.equals(dbType) || //            JdbcUtils.H2.equals(dbType)) {            if (config == null) {                config = new WallConfig(MySqlWallProvider.DEFAULT_CONFIG_DIR);            }                                                // 初始化Mysql类型的防火墙            provider = new MySqlWallProvider(config);        } else if (JdbcUtils.ORACLE.equals(dbType) || JdbcUtils.ALI_ORACLE.equals(dbType)) {            if (config == null) {                config = new WallConfig(OracleWallProvider.DEFAULT_CONFIG_DIR);            }

我们发现此方法加了 synchronized,并且里面有一个关键的属性 dbType。

由于WallFilter只有一个实例,此处的dbType是成员变量,代码中判断为null时才从当前dataSource中获取;

那么问题来了,我们现在有两个 DruidDataSource 实例,那么在初始化时就会调用WallFilter的初始化,但是尽管调用多次 WallFilterinit 方法,但是从上面代码得知dbType仅取决于第一次 DruidDataSource 调用WallFilter 的 init 方法时 dataSource.getDbType() 的值。

分析到这里,你看出什么问题了?

是的,如果当我们第一个访问的数据源是Mysql时,那么不管后续的DruidDataSource多次调用WallFilter 的 init 方法,dbType永远不会改变。

所以导致,我们用的永远都是 MySqlWallProvider ,也就是说我们用的SQL防火墙是Mysql类型的,所以后续对SQL的语法解析也用到了MySqlLexer 词法解析器,该解析器的关键字库里面是没有USING这个关键字的,所以会报错 USING 语法错误。

解决方案

开始我也说了,本次问题是部分实例报sql 异常,这又是为什么呢?

这是因为,这和你第一次访问的数据有关,如果第一次访问的是Mysql库,那后面在访问Oracle时候就会有这个问题。

如何解决这个问题呢?这里给出两种方案。

  1. 可以把spring.datasource.druid.filter.wall.enabled=true 这个配置设置成false 或去掉该配置,这也是最简单快捷的方式;

  2. 对WallFilter和StatFilter 两个类进行拓展和相关逻辑优化,需要编程,未实施。

以上就是Druid多数据源下Sql防火墙导致异常的示例分析,小编相信有部分知识点可能是我们日常工作会见到或用到的。希望你能通过这篇文章学到更多知识。更多详情敬请关注行业资讯频道。

0