温馨提示×

温馨提示×

您好,登录后才能下订单哦!

密码登录×
登录注册×
其他方式登录
点击 登录注册 即表示同意《亿速云用户服务条款》

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

发布时间:2021-09-10 17:28:10 来源:亿速云 阅读:341 作者:柒染 栏目:大数据

本篇文章给大家分享的是有关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 omitted
Caused 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 USING
at 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

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

关键问题在这里,为什么关键字的词里没有找到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防火墙导致异常的示例分析,小编相信有部分知识点可能是我们日常工作会见到或用到的。希望你能通过这篇文章学到更多知识。更多详情敬请关注亿速云行业资讯频道。

向AI问一下细节

免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。

AI