Failed to bind properties under ‘spring.datasource.druid‘ to javax.sql.DataSource;Nacos支持加密配置项动态刷新

2年前 (2022) 程序员胖胖胖虎阿
312 0 0

使用版本:

SpringBoot:2.1.18.RELEASE

SpringCloud: Greenwich.SR6

SpringCloud Alibaba: 2.1.4.RELEASE

jasypt-spring-boot-starter:2.1.2

druid-spring-boot-starter: 1.1.24

问题说明:

通过jasypt对Nacos中的敏感配置项进行加密处理后,当更新nacos更新配置项时,后台报错。

org.springframework.boot.context.properties.bind.BindException: Failed to bind properties under 'spring.datasource.druid' to javax.sql.DataSource
	at org.springframework.boot.context.properties.bind.Binder.handleBindError(Binder.java:242)
	at org.springframework.boot.context.properties.bind.Binder.bind(Binder.java:218)
	at org.springframework.boot.context.properties.bind.Binder.bind(Binder.java:202)
	at org.springframework.boot.context.properties.bind.Binder.bind(Binder.java:185)
	at org.springframework.boot.context.properties.ConfigurationPropertiesBinder.bind(ConfigurationPropertiesBinder.java:78)
	at org.springframework.boot.context.properties.ConfigurationPropertiesBindingPostProcessor.bind(ConfigurationPropertiesBindingPostProcessor.java:101)
	at org.springframework.boot.context.properties.ConfigurationPropertiesBindingPostProcessor.postProcessBeforeInitialization(ConfigurationPropertiesBindingPostProcessor.java:89)
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.applyBeanPostProcessorsBeforeInitialization(AbstractAutowireCapableBeanFactory.java:413)
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1761)
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:404)
	at org.springframework.cloud.context.properties.ConfigurationPropertiesRebinder.rebind(ConfigurationPropertiesRebinder.java:102)
	at org.springframework.cloud.context.properties.ConfigurationPropertiesRebinder.rebind(ConfigurationPropertiesRebinder.java:83)
	at org.springframework.cloud.context.properties.ConfigurationPropertiesRebinder.onApplicationEvent(ConfigurationPropertiesRebinder.java:128)
	at org.springframework.cloud.context.properties.ConfigurationPropertiesRebinder.onApplicationEvent(ConfigurationPropertiesRebinder.java:50)
	at org.springframework.context.event.SimpleApplicationEventMulticaster.doInvokeListener(SimpleApplicationEventMulticaster.java:172)
	at org.springframework.context.event.SimpleApplicationEventMulticaster.invokeListener(SimpleApplicationEventMulticaster.java:165)
	at org.springframework.context.event.SimpleApplicationEventMulticaster.multicastEvent(SimpleApplicationEventMulticaster.java:139)
	at org.springframework.context.support.AbstractApplicationContext.publishEvent(AbstractApplicationContext.java:403)
	at org.springframework.context.support.AbstractApplicationContext.publishEvent(AbstractApplicationContext.java:360)
	at org.springframework.cloud.context.refresh.ContextRefresher.refreshEnvironment(ContextRefresher.java:96)
	at org.springframework.cloud.context.refresh.ContextRefresher.refresh(ContextRefresher.java:85)
	at org.springframework.cloud.endpoint.event.RefreshEventListener.handle(RefreshEventListener.java:72)
	at org.springframework.cloud.endpoint.event.RefreshEventListener.onApplicationEvent(RefreshEventListener.java:61)
	at org.springframework.context.event.SimpleApplicationEventMulticaster.doInvokeListener(SimpleApplicationEventMulticaster.java:172)
	at org.springframework.context.event.SimpleApplicationEventMulticaster.invokeListener(SimpleApplicationEventMulticaster.java:165)
	at org.springframework.context.event.SimpleApplicationEventMulticaster.multicastEvent(SimpleApplicationEventMulticaster.java:139)
	at org.springframework.context.support.AbstractApplicationContext.publishEvent(AbstractApplicationContext.java:403)
	at org.springframework.context.support.AbstractApplicationContext.publishEvent(AbstractApplicationContext.java:360)
	at com.alibaba.cloud.nacos.refresh.NacosContextRefresher$1.innerReceive(NacosContextRefresher.java:133)
	at com.alibaba.nacos.api.config.listener.AbstractSharedListener.receiveConfigInfo(AbstractSharedListener.java:40)
	at com.alibaba.nacos.client.config.impl.CacheData$1.run(CacheData.java:210)
	at com.alibaba.nacos.client.config.impl.CacheData.safeNotifyListener(CacheData.java:241)
	at com.alibaba.nacos.client.config.impl.CacheData.checkListenerMd5(CacheData.java:181)
	at com.alibaba.nacos.client.config.impl.ClientWorker$LongPollingRunnable.run(ClientWorker.java:629)
	at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:511)
	at java.util.concurrent.FutureTask.run$$$capture(FutureTask.java:266)
	at java.util.concurrent.FutureTask.run(FutureTask.java)
	at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.access$201(ScheduledThreadPoolExecutor.java:180)
	at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:293)
	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
	at java.lang.Thread.run(Thread.java:745)
Caused by: java.lang.IllegalStateException: Unable to set value for property username
	at org.springframework.boot.context.properties.bind.JavaBeanBinder$BeanProperty.setValue(JavaBeanBinder.java:339)
	at org.springframework.boot.context.properties.bind.JavaBeanBinder.bind(JavaBeanBinder.java:86)
	at org.springframework.boot.context.properties.bind.JavaBeanBinder.bind(JavaBeanBinder.java:70)
	at org.springframework.boot.context.properties.bind.JavaBeanBinder.bind(JavaBeanBinder.java:54)
	at org.springframework.boot.context.properties.bind.Binder.lambda$null$4(Binder.java:329)
	at java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:193)
	at java.util.ArrayList$ArrayListSpliterator.tryAdvance(ArrayList.java:1351)
	at java.util.stream.ReferencePipeline.forEachWithCancel(ReferencePipeline.java:126)
	at java.util.stream.AbstractPipeline.copyIntoWithCancel(AbstractPipeline.java:498)
	at java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:485)
	at java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:471)
	at java.util.stream.FindOps$FindOp.evaluateSequential(FindOps.java:152)
	at java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234)
	at java.util.stream.ReferencePipeline.findFirst(ReferencePipeline.java:464)
	at org.springframework.boot.context.properties.bind.Binder.lambda$bindBean$5(Binder.java:330)
	at org.springframework.boot.context.properties.bind.Binder$Context.withIncreasedDepth(Binder.java:429)
	at org.springframework.boot.context.properties.bind.Binder$Context.withBean(Binder.java:415)
	at org.springframework.boot.context.properties.bind.Binder$Context.access$400(Binder.java:372)
	at org.springframework.boot.context.properties.bind.Binder.bindBean(Binder.java:328)
	at org.springframework.boot.context.properties.bind.Binder.bindObject(Binder.java:269)
	at org.springframework.boot.context.properties.bind.Binder.bind(Binder.java:214)
	... 40 common frames omitted
Caused by: java.lang.reflect.InvocationTargetException: null
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:498)
	at org.springframework.boot.context.properties.bind.JavaBeanBinder$BeanProperty.setValue(JavaBeanBinder.java:336)
	... 60 common frames omitted
Caused by: java.lang.UnsupportedOperationException: null
	at com.alibaba.druid.pool.DruidAbstractDataSource.setUsername(DruidAbstractDataSource.java:1123)
	... 65 common frames omitted

原因分析:

由于数据库用户名和密码采用了jasypt进行了加密处理,刷新nacos配置时,触发EnvironmentChangeEvent事件,触发JavaBeanBinder重新给DruidAbstractDataSource的username重新复制,这时候由于赋值的内容是加密过的(nacos不支持加密配置项动态刷新),和第一次赋值的解密的username不一样了,并且DruidAbstractDataSource的inited已经是初始化过了,导致触发UnsupportedOperationException的异常。

Failed to bind properties under ‘spring.datasource.druid‘ to javax.sql.DataSource;Nacos支持加密配置项动态刷新

解决思路:

从源码看,只要让setUsername中的StringUtils.equals(this.username, username))生效就可以避免该问题。

public void setUsername(String username) {
        //只要让该条件生效,就可以解决该问题
        if (StringUtils.equals(this.username, username)) {
            return;
        }

        if (inited) {
            throw new UnsupportedOperationException();
        }

        this.username = username;
    }

解决办法:

1.既然是username加密导致这个问题,最简单粗暴的就是取消username的jasypt加密

参考: 修改了Nacos配置后,Druid抛出大量数据库连接异常日志_legendaryhaha的博客-CSDN博客修改了Nacos配置后,Druid抛出大量数据库连接异常日志起因分析复现解决起因某个周末,运营同事反馈账单数据不对且客户需要开始执行交易了。起初,还以为只是简单的数据问题,核对一下就好了。但在生产日志排查的过程种,发现日志大量抛出了大量的数据库连接异常:ERROR DruidDataSource[run 2755] create connection SQLException异常信息下还有如下提示: Access denied for user 'root'@'X.XX.XX.XXX' (usFailed to bind properties under ‘spring.datasource.druid‘ to javax.sql.DataSource;Nacos支持加密配置项动态刷新https://blog.csdn.net/legendaryhaha/article/details/118458241

2.既然是由于刷新后赋值时,这个username没有被解密造成的,那么就让Nacos支持加密配置项动态刷新

参考:让Nacos支持加密配置项的配置刷新 - 剑握在手 - 博客园2021年4月1日(本内容非愚人节恶搞)新增: jasypt在新版本3.0.3.Release中已经解决了这个问题,请大家直接用新版就行。 传送门 ——————————————————————————Failed to bind properties under ‘spring.datasource.druid‘ to javax.sql.DataSource;Nacos支持加密配置项动态刷新https://www.cnblogs.com/flying607/p/12520009.html

按照这个文章进行修改,好像并没有解决,最后采用如下的代码使得Nacos支持加密配置项动态刷新:

/*
 * Copyright 2013-2018 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.alibaba.cloud.nacos.client;

import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;

import com.alibaba.cloud.nacos.NacosConfigManager;
import com.alibaba.cloud.nacos.NacosConfigProperties;
import com.alibaba.cloud.nacos.NacosPropertySourceRepository;
import com.alibaba.cloud.nacos.refresh.NacosContextRefresher;
import com.alibaba.nacos.api.config.ConfigService;
import com.ulisesbocchio.jasyptspringboot.EncryptablePropertyFilter;
import com.ulisesbocchio.jasyptspringboot.EncryptablePropertyResolver;
import com.ulisesbocchio.jasyptspringboot.EncryptablePropertySourceConverter;
import com.ulisesbocchio.jasyptspringboot.InterceptionMode;
import com.ulisesbocchio.jasyptspringboot.detector.DefaultPropertyDetector;
import com.ulisesbocchio.jasyptspringboot.filter.DefaultPropertyFilter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.cloud.bootstrap.config.PropertySourceLocator;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.core.annotation.Order;
import org.springframework.core.env.CompositePropertySource;
import org.springframework.core.env.Environment;
import org.springframework.core.env.MutablePropertySources;
import org.springframework.core.env.PropertySource;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;


/**
 * @author xiaojing
 * @author pbting
 * @Description 覆盖nacos三方包里面的类,解决采用jasypt加密nacos敏感配置项后二次刷新,ENC(*****)无法解析的问题
 */
@Order(0)
public class NacosPropertySourceLocator implements PropertySourceLocator {

    private static final Logger log = LoggerFactory
            .getLogger(NacosPropertySourceLocator.class);

    private static final String NACOS_PROPERTY_SOURCE_NAME = "NACOS";

    private static final String SEP1 = "-";

    private static final String DOT = ".";

    private NacosPropertySourceBuilder nacosPropertySourceBuilder;

    private NacosConfigProperties nacosConfigProperties;

    private NacosConfigManager nacosConfigManager;

    @Autowired
    private ConfigurableListableBeanFactory beanFactory;
    @Autowired
    private ConfigurableApplicationContext context;

    /**
     * com.ulisesbocchio.jasyptspringboot.configuration.EncryptablePropertyResolverConfiguration.RESOLVER_BEAN_NAME;
     */
    static final String RESOLVER_BEAN_NAME = "lazyEncryptablePropertyResolver";
    /**
     * com.ulisesbocchio.jasyptspringboot.configuration.EncryptablePropertyResolverConfiguration.FILTER_BEAN_NAME;
     */
    static final String FILTER_BEAN_NAME = "lazyEncryptablePropertyFilter";

    /**
     * recommend to use
     * {@link NacosPropertySourceLocator#NacosPropertySourceLocator(com.alibaba.cloud.nacos.NacosConfigManager)}.
     * @param nacosConfigProperties nacosConfigProperties
     */
    @Deprecated
    public NacosPropertySourceLocator(NacosConfigProperties nacosConfigProperties) {
        this.nacosConfigProperties = nacosConfigProperties;
    }

    public NacosPropertySourceLocator(NacosConfigManager nacosConfigManager) {
        this.nacosConfigManager = nacosConfigManager;
        this.nacosConfigProperties = nacosConfigManager.getNacosConfigProperties();
    }

    @Override
    public PropertySource<?> locate(Environment env) {
        nacosConfigProperties.setEnvironment(env);
        ConfigService configService = nacosConfigManager.getConfigService();

        if (null == configService) {
            log.warn("no instance of config service found, can't load config from nacos");
            return null;
        }
        long timeout = nacosConfigProperties.getTimeout();
        nacosPropertySourceBuilder = new NacosPropertySourceBuilder(configService,
                timeout);
        String name = nacosConfigProperties.getName();

        String dataIdPrefix = nacosConfigProperties.getPrefix();
        if (StringUtils.isEmpty(dataIdPrefix)) {
            dataIdPrefix = name;
        }

        if (StringUtils.isEmpty(dataIdPrefix)) {
            dataIdPrefix = env.getProperty("spring.application.name");
        }

        CompositePropertySource composite = new CompositePropertySource(
                NACOS_PROPERTY_SOURCE_NAME);

        loadSharedConfiguration(composite);
        loadExtConfiguration(composite);
        loadApplicationConfiguration(composite, dataIdPrefix, nacosConfigProperties, env);


        return composite;
    }

    /**
     * load shared configuration.
     */
    private void loadSharedConfiguration(
            CompositePropertySource compositePropertySource) {
        List<NacosConfigProperties.Config> sharedConfigs = nacosConfigProperties
                .getSharedConfigs();
        if (!CollectionUtils.isEmpty(sharedConfigs)) {
            checkConfiguration(sharedConfigs, "shared-configs");
            loadNacosConfiguration(compositePropertySource, sharedConfigs);
        }
    }

    /**
     * load extensional configuration.
     */
    private void loadExtConfiguration(CompositePropertySource compositePropertySource) {
        List<NacosConfigProperties.Config> extConfigs = nacosConfigProperties
                .getExtensionConfigs();
        if (!CollectionUtils.isEmpty(extConfigs)) {
            checkConfiguration(extConfigs, "extension-configs");
            loadNacosConfiguration(compositePropertySource, extConfigs);
        }
    }

    /**
     * load configuration of application.
     */
    private void loadApplicationConfiguration(
            CompositePropertySource compositePropertySource, String dataIdPrefix,
            NacosConfigProperties properties, Environment environment) {
        String fileExtension = properties.getFileExtension();
        String nacosGroup = properties.getGroup();
        // load directly once by default
        loadNacosDataIfPresent(compositePropertySource, dataIdPrefix, nacosGroup,
                fileExtension, true);
        // load with suffix, which have a higher priority than the default
        loadNacosDataIfPresent(compositePropertySource,
                dataIdPrefix + DOT + fileExtension, nacosGroup, fileExtension, true);
        // Loaded with profile, which have a higher priority than the suffix
        for (String profile : environment.getActiveProfiles()) {
            String dataId = dataIdPrefix + SEP1 + profile + DOT + fileExtension;
            loadNacosDataIfPresent(compositePropertySource, dataId, nacosGroup,
                    fileExtension, true);
        }

    }

    private void loadNacosConfiguration(final CompositePropertySource composite,
                                        List<NacosConfigProperties.Config> configs) {
        for (NacosConfigProperties.Config config : configs) {
            String dataId = config.getDataId();
            String fileExtension = dataId.substring(dataId.lastIndexOf(DOT) + 1);
            loadNacosDataIfPresent(composite, dataId, config.getGroup(), fileExtension,
                    config.isRefresh());
        }
    }

    private void checkConfiguration(List<NacosConfigProperties.Config> configs,
                                    String tips) {
        for (int i = 0; i < configs.size(); i++) {
            String dataId = configs.get(i).getDataId();
            if (dataId == null || dataId.trim().length() == 0) {
                throw new IllegalStateException(String.format(
                        "the [ spring.cloud.nacos.config.%s[%s] ] must give a dataId",
                        tips, i));
            }
        }
    }

    private void loadNacosDataIfPresent(final CompositePropertySource composite,
                                        final String dataId, final String group, String fileExtension,
                                        boolean isRefreshable) {
        if (null == dataId || dataId.trim().length() < 1) {
            return;
        }
        if (null == group || group.trim().length() < 1) {
            return;
        }
        NacosPropertySource propertySource = this.loadNacosPropertySource(dataId, group,
                fileExtension, isRefreshable);

        //主要是结合jasypt的实现原理,对nacos生成的propertySource进行包装,包装成EncryptableXXX
        EncryptablePropertyResolver propertyResolver = beanFactory.getBean(RESOLVER_BEAN_NAME, EncryptablePropertyResolver.class);
        EncryptablePropertyFilter encryptablePropertyFilter = beanFactory.getBean(FILTER_BEAN_NAME,EncryptablePropertyFilter.class);
        propertySource = (NacosPropertySource)  EncryptablePropertySourceConverter.makeEncryptable(InterceptionMode.PROXY, propertyResolver,encryptablePropertyFilter, propertySource);

        this.addFirstPropertySource(composite, propertySource, false);
    }

    private NacosPropertySource loadNacosPropertySource(final String dataId,
                                                        final String group, String fileExtension, boolean isRefreshable) {
        if (NacosContextRefresher.getRefreshCount() != 0) {
            if (!isRefreshable) {
                return NacosPropertySourceRepository.getNacosPropertySource(dataId,
                        group);
            }
        }
        return nacosPropertySourceBuilder.build(dataId, group, fileExtension,
                isRefreshable);
    }

    /**
     * Add the nacos configuration to the first place and maybe ignore the empty
     * configuration.
     */
    private void addFirstPropertySource(final CompositePropertySource composite,
                                        NacosPropertySource nacosPropertySource, boolean ignoreEmpty) {
        if (null == nacosPropertySource || null == composite) {
            return;
        }
        if (ignoreEmpty && nacosPropertySource.getSource().isEmpty()) {
            return;
        }
        composite.addFirstPropertySource(nacosPropertySource);
    }

    public void setNacosConfigManager(NacosConfigManager nacosConfigManager) {
        this.nacosConfigManager = nacosConfigManager;
    }

}

采用上面的代码后,再次刷新nacos配置,可以看到 username已经被解密,StringUtils.equals(this.username, username) 条件生效。

Failed to bind properties under ‘spring.datasource.druid‘ to javax.sql.DataSource;Nacos支持加密配置项动态刷新

相关文章

暂无评论

暂无评论...