整合Nacos和Druid(password使用密文)出现新建连接被拒绝情况
问题描述
Jmeter进行服务压测时出现,或因一段时间未操作数据库断开连接后再次请求建立连接时,服务器拒绝新的连接情况。
Caused by: com.mysql.cj.exceptions.CJException: Access denied for user 'appblog'@'192.168.1.10' (using password: YES)
原因分析
初一看就是密码错误,数据库配置如下:
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刷新后,会打印如下日志:
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
:
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刷新会回调DruidDataSource
的setPassword
方法,并且密码发生改变,问题已经确定,就是密码错误
切入点:config过滤器
因配置文件中Druid配置的filters
是config
,定位到源码com.alibaba.druid.filter.config.ConfigFilter
init方法是数据库连接池初始化的位置,能看到对密码进行了解密操作
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成员变量中
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);
}
}
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、一直等待到出现日志
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
方法并注入
/**
* 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);
}
}
@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);
}
}
}
或者
@Configuration
public class MyDruidDataSourceConfig {
@Autowired
private MyDruidDataSource myDruidDataSource;
@Bean("dataSource")
public DataSource druidDataSource() {
return myDruidDataSource;
}
}
版权声明:
作者:Joe.Ye
链接:https://www.appblog.cn/index.php/2023/04/01/new-connection-rejected-due-to-integration-of-nacos-and-druid-password-using-ciphertext/
来源:APP全栈技术分享
文章版权归作者所有,未经允许请勿转载。
共有 0 条评论