整合Nacos和Druid(password使用密文)出现新建连接被拒绝情况

问题描述

Jmeter进行服务压测时出现,或因一段时间未操作数据库断开连接后再次请求建立连接时,服务器拒绝新的连接情况。

1
Caused by: com.mysql.cj.exceptions.CJException: Access denied for user 'appblog'@'192.168.1.10' (using password: YES)

原因分析

初一看就是密码错误,数据库配置如下:

1
2
3
4
5
6
7
8
9
10
spring:
datasource:
appblog:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://192.168.1.10:3306/appblog?useUnicode=true&autoReconnect=true&characterEncoding=utf-8
username: appblog
password: Up58k0xJr7C2kcVFTGrnxRlyPBsj7DPeKjMYUAHxWQfjighJLheMrDIlp7Xj8r5Ad1I8Q+qh5WwnCv5kFyWlTQ==
filters: config,myfilter
connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=3000;config.decrypt=true;config.decrypt.key=MFwwDQYJKoZIhvcNa5EBkQADSwAwSAJ6ALYHjdmAVAm79Ao3MbruNxsWM76Ifz+qaN8sOZesMKvYCJdpxLFLtmo6bNkpYkzk+OAYhXf7U8r0+dZngOy0RnMCAwEAAQ==

情况分析:

1、项目启动是正常的,启动后可以访问数据库,说明数据库连接池配置没问题
2、但为什么在压测或者隔夜之后,就出现数据库连接异常,数据库拒绝连接
3、根据异常提示,是数据库密码错误,但是奇怪为什么项目开始启动正常,压测或隔夜后,密码就错了
4、开启远程调试,先处理压测情况

切入点:Nacos刷新日志

Nacos刷新后,会打印如下日志:

1
2020-12-22 13:15:04.700 [ok-cloud-mall-service][INFO] [com.alibaba.druid.pool.DruidAbstractDataSource] [setPassword] [1146] : password changed

提示密码已更新,定位到源码com.alibaba.druid.pool.DruidAbstractDataSource

1
2
3
4
5
6
7
8
9
10
11
12
13
public abstract class DruidAbstractDataSource ... {

...

public void setPassword(String password) {
if (!StringUtils.equals(this.password, password)) {
if (this.inited) {
LOG.info("password changed");
}

this.password = password;
}
}

由此可知,Nacos刷新会回调DruidDataSourcesetPassword方法,并且密码发生改变,问题已经确定,就是密码错误

切入点:config过滤器

因配置文件中Druid配置的filtersconfig,定位到源码com.alibaba.druid.filter.config.ConfigFilter

init方法是数据库连接池初始化的位置,能看到对密码进行了解密操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
public class ConfigFilter extends FilterAdapter {
private static Log LOG = LogFactory.getLog(ConfigFilter.class);
public static final String CONFIG_FILE = "config.file";
public static final String CONFIG_DECRYPT = "config.decrypt";
public static final String CONFIG_KEY = "config.decrypt.key";
public static final String SYS_PROP_CONFIG_FILE = "druid.config.file";
public static final String SYS_PROP_CONFIG_DECRYPT = "druid.config.decrypt";
public static final String SYS_PROP_CONFIG_KEY = "druid.config.decrypt.key";

public ConfigFilter() {
}

public void init(DataSourceProxy dataSourceProxy) {
if (!(dataSourceProxy instanceof DruidDataSource)) {
LOG.error("ConfigLoader only support DruidDataSource");
}

DruidDataSource dataSource = (DruidDataSource) dataSourceProxy;
Properties connectionProperties = dataSource.getConnectProperties();

Properties configFileProperties = loadPropertyFromConfigFile(connectionProperties);

// 判断是否需要解密,如果需要就进行解密行动
boolean decrypt = isDecrypt(connectionProperties, configFileProperties);

if (configFileProperties == null) {
if (decrypt) {
//密码解码操作
decrypt(dataSource, null);
}
return;
}

if (decrypt) {
decrypt(dataSource, configFileProperties);
}

try {
DruidDataSourceFactory.config(dataSource, configFileProperties);
} catch (SQLException e) {
throw new IllegalArgumentException("Config DataSource error.", e);
}
}

往下走,随后看到passwordPlainText为密码的明文(解密后),并且解密后的明文密码,设置到了password成员变量中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public void decrypt(DruidDataSource dataSource, Properties info) {

try {
String encryptedPassword = null;
if (info != null) {
encryptedPassword = info.getProperty(DruidDataSourceFactory.PROP_PASSWORD);
}

if (encryptedPassword == null || encryptedPassword.length() == 0) {
encryptedPassword = dataSource.getConnectProperties().getProperty(DruidDataSourceFactory.PROP_PASSWORD);
}

if (encryptedPassword == null || encryptedPassword.length() == 0) {
encryptedPassword = dataSource.getPassword();
}

PublicKey publicKey = getPublicKey(dataSource.getConnectProperties(), info);

String passwordPlainText = ConfigTools.decrypt(publicKey, encryptedPassword);

if (info != null) {
info.setProperty(DruidDataSourceFactory.PROP_PASSWORD, passwordPlainText);
} else {
dataSource.setPassword(passwordPlainText);
}
} catch (Exception e) {
throw new IllegalArgumentException("Failed to decrypt.", e);
}
}
1
2
3
4
5
6
7
8
9
10
11
public abstract class DruidAbstractDataSource ... {

public void setPassword(String password) {
if (!StringUtils.equals(this.password, password)) {
if (this.inited) {
LOG.info("password changed");
}

this.password = password;
}
}

又回到Nacos刷新后日志打印的地方,即password changed,我们可以猜测,解密只在Druid初始化时进行,初始化时使用解密后的明文连接数据库,Nacos刷新后新的连接则直接使用未解密的密文连接数据库,导致连接失败

场景梳理

1、开始正常解密,并使用明文,成功创建连接池
2、所以项目启动后一切正常
3、随后Nacos配置刷新,密码被重新set成了密文
4、那么在压测(当前连接数不够,需要重新创建)、或者长时间不调用(连接释放断开,需要重连)之后,需要重新创建连接,这时候就拿的是密文去请求访问数据库,自然密码不对

项目启动时,Nacos中的配置加载了一次,数据库连接池初始化成功,而后因为人为手动刷新Nacos配置或是某种原因导致Nacos配置多次读取。因为在源码中看到,初始化连接池的时候会对密文进行解密。而第二次并未进行解密,只是将密文set进了password。debugger源码发现,也只有在初始化连接池的init()方法之才会进行解密。

第二次加载配置,此时DataSource已经完成了初始化,并不会再次触发初始化,所以密文并未被解密。

特别注意,如果Nacos中有配置refreshable-dataids,会在项目启动完毕后,再加载一次配置。而就是此次加载,数据库连接池已经初始化成功,并不再进行二次初始化,所以密文并未被解密,直接set进了password。导致在连接不够或者重连时,使用的密码错误。

复现步骤

1、应用重启后修改Nacos配置

2、一直等待到出现日志

1
2020-12-21 11:48:30.392 [ok-cloud-mall-service][ ERROR] [118336] [nio-8701-exec-3] [bfdcee1a80aac39a] [bfdcee1a80aac39a] [true] --- [com.alibaba.druid.pool.DruidAbstractDataSource] [testConnectionInternal] [1588] : discard long time none received connection. , jdbcUrl : jdbc:mysql://192.168.1.10:3306/appblog?useUnicode=true&autoReconnect=true&characterEncoding=utf-8, jdbcUrl : jdbc:mysql://192.168.1.10:3306/appblog?useUnicode=true&autoReconnect=true&characterEncoding=utf-8, lastPacketReceivedIdleMillis : 89618

3、立即就会报错(或者重新请求建立数据库连接,如登录等)

解决方法

重写DataSource的setPassword方法并注入

参考:https://github.com/alibaba/druid/blob/master/druid-spring-boot-starter/src/main/java/com/alibaba/druid/spring/boot/autoconfigure/DruidDataSourceWrapper.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
/**
* Druid 数据库密码只在数据库 第一次初始化时解密
* Nacos 刷新时会将“没有解密的密文”重新赋值予DataSource
* 数据库在重建连接时报错,无法连接
*/
@Slf4j
@Component
public class MyDruidDataSource extends DruidDataSource implements InitializingBean {
@Autowired
private DataSourceProperties basicProperties;

@Override
public void afterPropertiesSet() throws Exception {
//if not found prefix 'spring.datasource.druid' jdbc properties ,'spring.datasource' prefix jdbc properties will be used.
if (super.getUsername() == null) {
super.setUsername(basicProperties.determineUsername());
}
if (super.getPassword() == null) {
super.setPassword(basicProperties.determinePassword());
}
if (super.getUrl() == null) {
super.setUrl(basicProperties.determineUrl());
}
if (super.getDriverClassName() == null) {
super.setDriverClassName(basicProperties.getDriverClassName());
}
}

@Autowired(required = false)
public void autoAddFilters(List<Filter> filters) {
super.filters.addAll(filters);
}

@Override
public void setMaxEvictableIdleTimeMillis(long maxEvictableIdleTimeMillis) {
try {
super.setMaxEvictableIdleTimeMillis(maxEvictableIdleTimeMillis);
} catch (IllegalArgumentException ignore) {
super.maxEvictableIdleTimeMillis = maxEvictableIdleTimeMillis;
}
}

@Override
public void setPassword(String password) {
if (!this.inited) {
super.setPassword(password);
} else if (filters != null) {
try {
super.setPassword(ConfigTools.decrypt(getPublicKey(this.getConnectProperties()), password));
} catch (Exception e) {
log.warn("DataSource password decrypt error.", e);
super.setPassword(password);
}
}
}

public PublicKey getPublicKey(Properties connectionProperties) {
String key = connectionProperties.getProperty("config.decrypt.key");

if (StringUtils.isEmpty(key)) {
key = System.getProperty("druid.config.decrypt.key");
}

return ConfigTools.getPublicKey(key);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Slf4j
@Component
public class MyDruidFilter extends FilterAdapter {

@Autowired
private MyDruidDataSource myDruidDataSource;

@Override
public void init(DataSourceProxy dataSourceProxy) {
if (!(dataSourceProxy instanceof DruidDataSource)) {
log.error("ConfigLoader only support DruidDataSource");
}
DruidDataSource dataSource = (DruidDataSource) dataSourceProxy;
log.info("db configuration: url=" + dataSource.getUrl());

Properties properties = dataSource.getConnectProperties();
try {
// 将信息配置进Druid
DruidDataSourceFactory.config(myDruidDataSource, properties);
} catch (Exception e) {
log.error("DataSource config error.", e);
}
}
}

或者

1
2
3
4
5
6
7
8
9
10
11
@Configuration
public class MyDruidDataSourceConfig {

@Autowired
private MyDruidDataSource myDruidDataSource;

@Bean("dataSource")
public DataSource druidDataSource() {
return myDruidDataSource;
}
}

Powered by AppBlog.CN     浙ICP备14037229号

Copyright © 2012 - 2021 APP开发技术博客 All Rights Reserved.

访客数 : | 访问量 :