spring cloud + nacos 配置刷新 jasypt 未解密 数据库连接失败1045

现象

  1. 修改了nacos配置,重新发布
  2. 过了一段时间发现,数据库偶尔会连接失败报异常
- create connection SQLException, url: jdbc:mysql://*******:3306/settlement?useUnicode=true&characterEncoding=UTF-8&allowMultiQueries=true&useSSL=false&zeroDateTimeBehavior=convertToNull&serverTimezone=Asia/Shanghai, errorCode 1045, state 28000java.sql.SQLException: Access denied for user '****'@'ip' (using password: YES)
    at com.mysql.cj.jdbc.exceptions.SQLError.createSQLException(SQLError.java:129)
    at com.mysql.cj.jdbc.exceptions.SQLError.createSQLException(SQLError.java:97)
    at com.mysql.cj.jdbc.exceptions.SQLExceptionsMapping.translateException(SQLExceptionsMapping.java:122)
    at com.mysql.cj.jdbc.ConnectionImpl.createNewIO(ConnectionImpl.java:836)
    at com.mysql.cj.jdbc.ConnectionImpl.<init>(ConnectionImpl.java:456)
    at com.mysql.cj.jdbc.ConnectionImpl.getInstance(ConnectionImpl.java:246)
    at com.mysql.cj.jdbc.NonRegisteringDriver.connect(NonRegisteringDriver.java:199)
    at com.alibaba.druid.filter.FilterChainImpl.connection_connect(FilterChainImpl.java:156)
    at com.alibaba.druid.filter.FilterAdapter.connection_connect(FilterAdapter.java:787)
    at com.alibaba.druid.filter.FilterEventAdapter.connection_connect(FilterEventAdapter.java:38)
    at com.alibaba.druid.filter.FilterChainImpl.connection_connect(FilterChainImpl.java:150)
    at com.alibaba.druid.filter.stat.StatFilter.connection_connect(StatFilter.java:218)
    at com.alibaba.druid.filter.FilterChainImpl.connection_connect(FilterChainImpl.java:150)
    at com.alibaba.druid.filter.FilterAdapter.connection_connect(FilterAdapter.java:787)
    at com.alibaba.druid.filter.FilterChainImpl.connection_connect(FilterChainImpl.java:150)
    at com.alibaba.druid.pool.DruidAbstractDataSource.createPhysicalConnection(DruidAbstractDataSource.java:1646)
    at com.alibaba.druid.pool.DruidAbstractDataSource.createPhysicalConnection(DruidAbstractDataSource.java:1710)
    at com.alibaba.druid.pool.DruidDataSource$CreateConnectionThread.run(DruidDataSource.java:2753)

环境

  1. mysql 5.7.28-log
  2. maven pom.xml


<dependency>
    <groupId>com.github.ulisesbocchio</groupId>
    <artifactId>jasypt-spring-boot-starter</artifactId>
    <version>2.1.2</version>
</dependency>

 <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        <version>2.2.0.RELEASE</version>
 </dependency>    

<dependency>
     <groupId>com.alibaba</groupId>
     <artifactId>druid-spring-boot-starter</artifactId>
      <version>1.1.21</version>
</dependency>
  1. bootstrap.yml 配置
spring:
  application:
    name: test
  cloud:
    nacos:
      serveraddr: ***
      namespace: loc
      config:
        server-addr: ${spring.cloud.nacos.serveraddr}
        namespace: ${spring.cloud.nacos.namespace}
        prefix: ${spring.application.name}
        file-extension: yml
      discovery:
        server-addr: ${spring.cloud.nacos.serveraddr}
        namespace: ${spring.cloud.nacos.namespace}
  1. nacos配置
    test.yml
jasypt:
  encryptor:
    password: hello

分析

发布nacos配置会导致配置刷新吗?

如下代码可以清楚看到和bootstrap.yml对应的NacosConfigProperties ,默认是开启刷新的。

com.alibaba.cloud.nacos.NacosConfigProperties


    /**
     * the master switch for refresh configuration, it default opened(true).
     */
    private boolean refreshEnabled = true;

数据库为啥连接失败?

启动时数据库创建是成功的,为啥现在偶尔失败。

打个断点看一看

com.alibaba.druid.pool.DruidAbstractDataSource#createPhysicalConnection()

       // 断点
        String password = getPassword();
        PasswordCallback passwordCallback = getPasswordCallback();

断点处发现 密码变成了 ENC()加密数据 ,密码根本未解密才导致连接失败。

什么时候密码被变成未解密的了?

修改nacos上的配置,重新发布
com.alibaba.druid.pool.DruidAbstractDataSource#setPassword
打个断点看一看

    public void setPassword(String password) {
     // 断点
        if (StringUtils.equals(this.password, password)) {
            return;
        }

        if (inited) {
            LOG.info("password changed");
        }

        this.password = password;
    }

堆栈如下,收到NacosContextRefresher事件后,动态刷新触发了密码的修改

setPassword:1134, DruidAbstractDataSource (com.alibaba.druid.pool)
invoke:-1, GeneratedMethodAccessor502 (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:498, Method (java.lang.reflect)
setValue:346, JavaBeanBinder$BeanProperty (org.springframework.boot.context.properties.bind)
bind:96, JavaBeanBinder (org.springframework.boot.context.properties.bind)
bind:79, JavaBeanBinder (org.springframework.boot.context.properties.bind)
bind:56, JavaBeanBinder (org.springframework.boot.context.properties.bind)
lambda$bindDataObject$5:452, Binder (org.springframework.boot.context.properties.bind)
get:-1, 540325452 (org.springframework.boot.context.properties.bind.Binder$$Lambda$42)
withIncreasedDepth:570, Binder$Context (org.springframework.boot.context.properties.bind)
withDataObject:556, Binder$Context (org.springframework.boot.context.properties.bind)
access$400:513, Binder$Context (org.springframework.boot.context.properties.bind)
bindDataObject:450, Binder (org.springframework.boot.context.properties.bind)
bindObject:391, Binder (org.springframework.boot.context.properties.bind)
bind:320, Binder (org.springframework.boot.context.properties.bind)
bind:308, Binder (org.springframework.boot.context.properties.bind)
bind:238, Binder (org.springframework.boot.context.properties.bind)
bind:225, Binder (org.springframework.boot.context.properties.bind)
bind:89, ConfigurationPropertiesBinder (org.springframework.boot.context.properties)
bind:107, ConfigurationPropertiesBindingPostProcessor (org.springframework.boot.context.properties)
postProcessBeforeInitialization:96, ConfigurationPropertiesBindingPostProcessor (org.springframework.boot.context.properties)
applyBeanPostProcessorsBeforeInitialization:416, AbstractAutowireCapableBeanFactory (org.springframework.beans.factory.support)
initializeBean:1795, AbstractAutowireCapableBeanFactory (org.springframework.beans.factory.support)
initializeBean:407, AbstractAutowireCapableBeanFactory (org.springframework.beans.factory.support)
rebind:108, ConfigurationPropertiesRebinder (org.springframework.cloud.context.properties)
rebind:84, ConfigurationPropertiesRebinder (org.springframework.cloud.context.properties)
onApplicationEvent:142, ConfigurationPropertiesRebinder (org.springframework.cloud.context.properties)
onApplicationEvent:51, ConfigurationPropertiesRebinder (org.springframework.cloud.context.properties)
doInvokeListener:172, SimpleApplicationEventMulticaster (org.springframework.context.event)
invokeListener:165, SimpleApplicationEventMulticaster (org.springframework.context.event)
multicastEvent:139, SimpleApplicationEventMulticaster (org.springframework.context.event)
publishEvent:403, AbstractApplicationContext (org.springframework.context.support)
publishEvent:360, AbstractApplicationContext (org.springframework.context.support)
refreshEnvironment:96, ContextRefresher (org.springframework.cloud.context.refresh)
refresh:85, ContextRefresher (org.springframework.cloud.context.refresh)
handle:72, RefreshEventListener (org.springframework.cloud.endpoint.event)
onApplicationEvent:61, RefreshEventListener (org.springframework.cloud.endpoint.event)
doInvokeListener:172, SimpleApplicationEventMulticaster (org.springframework.context.event)
invokeListener:165, SimpleApplicationEventMulticaster (org.springframework.context.event)
multicastEvent:139, SimpleApplicationEventMulticaster (org.springframework.context.event)
publishEvent:403, AbstractApplicationContext (org.springframework.context.support)
publishEvent:360, AbstractApplicationContext (org.springframework.context.support)
innerReceive:133, NacosContextRefresher$1 (com.alibaba.cloud.nacos.refresh)
receiveConfigInfo:38, AbstractSharedListener (com.alibaba.nacos.api.config.listener)
run:203, CacheData$1 (com.alibaba.nacos.client.config.impl)
safeNotifyListener:233, CacheData (com.alibaba.nacos.client.config.impl)
checkListenerMd5:174, CacheData (com.alibaba.nacos.client.config.impl)
run:552, ClientWorker$LongPollingRunnable (com.alibaba.nacos.client.config.impl)
call:511, Executors$RunnableAdapter (java.util.concurrent)
run$$$capture:266, FutureTask (java.util.concurrent)
run:-1, FutureTask (java.util.concurrent)

如何解决?

1. 不刷新

bootstrap.yml 配置
spring.cloud.nacos.config.refreshEnabled 为false

spring:
  application:
    name: test
  cloud:
    nacos:
      serveraddr: ***
      namespace: loc
      config:
        server-addr: ${spring.cloud.nacos.serveraddr}
        namespace: ${spring.cloud.nacos.namespace}
        prefix: ${spring.application.name}
        file-extension: yml
        refreshEnabled: false
      discovery:
        server-addr: ${spring.cloud.nacos.serveraddr}
        namespace: ${spring.cloud.nacos.namespace}

2. 让获取的property是解密后

梳理一下事件和动作点

时刻 动作
T1 我们点击发布新的nacos配置
T2 触发ContextRefresh,更新本地的属性
T3 连接池重新连接数据库

复现

在T1,T2时间之间,我kill掉了当前的connection。

因为是连接池,可能原来connection继续使用,这样无法看到报错,我需要kill掉之前的connection。

为了方便kill,我将druid初始化连接数,最小连接数都改为1。

mysql kill connection

// 查看当前connection
show processlist;
// 杀掉当前连接
kill pid;

refresh() 刷新

org.springframework.cloud.context.refresh.ContextRefresher

public synchronized Set<String> refresh() {
   Set<String> keys = refreshEnvironment();
   this.scope.refreshAll();
   return keys;
}

public synchronized Set<String> refreshEnvironment() {
   Map<String, Object> before = extract(
         this.context.getEnvironment().getPropertySources());
   addConfigFilesToEnvironment();
   // 哪些属性修改了,哪些Bean需要重新创建
   Set<String> keys = changes(before,
         extract(this.context.getEnvironment().getPropertySources())).keySet();
   this.context.publishEvent(new EnvironmentChangeEvent(this.context, keys));
   return keys;
}

debug看一下,可以看到是加密的PropertySource

image.png

在调用 addConfigFilesToEnvironment 之后,可以看到此时是不加密的PropertySource了。

image.png

nacos Property 如何刷新

com.alibaba.cloud.nacos.client.NacosPropertySourceLocator#loadNacosDataIfPresent

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);
   this.addFirstPropertySource(composite, propertySource, false);
}

可以从图上看到核心的问题是NacosPropertySource没有被
EncryptableEnumerablePropertySourceWrapper 装饰,导致了获取到属性是未加密的。

可以从
org.springframework.cloud.context.refresh.ContextRefresher
看到触发了 EnvironmentChangeEvent 事件,所以解决方法是我们先处理EnvironmentChangeEvent事件,将NacosPropertySource装饰为EncryptableEnumerablePropertySourceWrapper

官方解决方案

jasypt-spring-boot-parent-3.0.3

image.png
1. 升级版本到v3.0.3
<dependency> 
    <groupId>com.github.ulisesbocchio</groupId> 
    <artifactId>jasypt-spring-boot-starter</artifactId> 
    <version>3.0.3<version> 
</dependency>
2. nacos test.yml 新增配置
jasypt:
  encryptor:
    password: hello
    # 新增配置
    algorithm: PBEWithMD5AndDES
    iv-generator-classname: org.jasypt.iv.NoIvGenerator

源码解析

我拉了v3.0.2和v3.0.3对比,

RefreshScopeRefreshedEventListener 是一个处理ApplicationEvent的Listener,

v3.0.3 新增了对
org.springframework.cloud.context.environment.EnvironmentChangeEvent的处理

image.png

V3.0.3 RefreshScopeRefreshedEventListener 代码如下

package com.ulisesbocchio.jasyptspringboot.caching;

import com.ulisesbocchio.jasyptspringboot.EncryptablePropertySource;
import com.ulisesbocchio.jasyptspringboot.EncryptablePropertySourceConverter;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.ApplicationEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.core.env.*;
import org.springframework.util.ClassUtils;

@Order(Ordered.HIGHEST_PRECEDENCE)
@Slf4j
public class RefreshScopeRefreshedEventListener implements ApplicationListener<ApplicationEvent> {

    public static final String REFRESHED_EVENT_CLASS = "org.springframework.cloud.context.scope.refresh.RefreshScopeRefreshedEvent";
    public static final String ENVIRONMENT_EVENT_CLASS = "org.springframework.cloud.context.environment.EnvironmentChangeEvent";
    private final ConfigurableEnvironment environment;
    private final EncryptablePropertySourceConverter converter;

    public RefreshScopeRefreshedEventListener(ConfigurableEnvironment environment, EncryptablePropertySourceConverter converter) {
        this.environment = environment;
        this.converter = converter;
    }

    @Override
    @SneakyThrows
    public void onApplicationEvent(ApplicationEvent event) {
        if (isAssignable(ENVIRONMENT_EVENT_CLASS, event) || isAssignable(REFRESHED_EVENT_CLASS, event)) {
            log.info("Refreshing cached encryptable property sources");
            refreshCachedProperties();
            decorateNewSources();
        }
    }

    private void decorateNewSources() {
        // 将新的PropertySource转为EncryptablePropertySource

        MutablePropertySources propSources = environment.getPropertySources();
        converter.convertPropertySources(propSources);
    }

    boolean isAssignable(String className, Object value) {
        try {
            return ClassUtils.isAssignableValue(ClassUtils.forName(className, null), value);
        } catch (ClassNotFoundException e) {
            return false;
        }
    }

    private void refreshCachedProperties() {
        PropertySources propertySources = environment.getPropertySources();
        propertySources.forEach(this::refreshPropertySource);
    }

    @SuppressWarnings("rawtypes")
    private void refreshPropertySource(PropertySource<?> propertySource) {
        if (propertySource instanceof CompositePropertySource) {
            CompositePropertySource cps = (CompositePropertySource) propertySource;
            cps.getPropertySources().forEach(this::refreshPropertySource);
        } else if (propertySource instanceof EncryptablePropertySource) {
            EncryptablePropertySource eps = (EncryptablePropertySource) propertySource;
            eps.refresh();
        }
    }
}

参考资料

jasypt-spring-boot-parent-3.0.3

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,033评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,725评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,473评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,846评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,848评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,691评论 1 282
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,053评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,700评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,856评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,676评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,787评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,430评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,034评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,990评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,218评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,174评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,526评论 2 343

推荐阅读更多精彩内容