最近接了一个外包单(基于springboot2,连接池为druid),客户经费有限,基本上要啥,啥没有,项目基本上是托管在私人的某gay,某云等,本着让客户放心的原则,就在安全方面多考虑了一点,首先比如数据库密码加密之类的,虽然要是有心要破解也是容易,但至少加密给自己心里一点暗示。。。废话有点多,进入正题,本文主要分为3个部分,第一部分是单个数据源密码加密,第二部分是多个数据源密码加密,第三部分是简要的解密源码分析。
https://mvnrepository.com/artifact/com.alibaba/druid
我这边下载的版本是如下
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.10</version>
</dependency>
这个jar主要是用来生成加密密码
利用刚才下好的jar,在cmd中执行如下命令
java -cp druid-1.1.10.jar com.alibaba.druid.filter.config.ConfigTools test
注:test为你数据库的密码
对我们有用的是publicKey和加密后的password,这个publickey主要是用来解密的秘钥
spring:
datasource:
type: com.alibaba.druid.pool.DruidDataSource
driverClassName: com.mysql.jdbc.Driver
publicKey: MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKXfJyPsQ1rvSQXO+8m1TrIWS5XSSwzwDBIjPGZNbpZ10+Tai7k1GMzF6eufgMNWlNwOHJvxIYwjrts8b4UbSiECAwEAAQ==
druid:
url: jdbc:mysql://lcoahost:3306/test1?allowMultiQueries=true&useUnicode=true&characterEncoding=UTF-8&useSSL=false
username: root
password: L/Jlcu+tiIgvq9wEvnycxvEE3+RVixnY/YgUB/5mAdO1WLdlrt2CipYxGjnS/4A+NtR0TTldmItzY4UtbSRe6g==
initial-size: 10
max-active: 100
min-idle: 10
max-wait: 60000
pool-prepared-statements: true
max-pool-prepared-statement-per-connection-size: 20
time-between-eviction-runs-millis: 60000
min-evictable-idle-time-millis: 300000
#validation-query: SELECT 1 FROM DUAL
test-while-idle: true
test-on-borrow: false
test-on-return: false
stat-view-servlet:
enabled: true
url-pattern: /druid/*
#login-username: admin
#login-password: admin
filter:
stat:
log-slow-sql: true
slow-sql-millis: 1000
merge-sql: false
wall:
config:
multi-statement-allow: true
config:
enabled: true
connection-properties: config.decrypt=true;config.decrypt.key=${spring.datasource.publicKey}
spring.datasource.druid.connection-properties
这个属性配置的value是键值对,其中config.decrypt=true表示要进行解密,config.decrypt.key=${spring.datasource.publicKey}注入要解密需要的公钥
spring.datasource.druid.filter.config.enabled=true
开启configFilter,这个不开启是没办法进行解密操作的
这个步骤和单数据源密码加密一样,就略过
spring:
datasource:
type: com.alibaba.druid.pool.DruidDataSource
driverClassName: com.mysql.jdbc.Driver
publicKey: MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKXfJyPsQ1rvSQXO+8m1TrIWS5XSSwzwDBIjPGZNbpZ10+Tai7k1GMzF6eufgMNWlNwOHJvxIYwjrts8b4UbSiECAwEAAQ==
druid:
first: #数据源1
url: jdbc:mysql://localhost:3306/test1?allowMultiQueries=true&useUnicode=true&characterEncoding=UTF-8&useSSL=false
username: root
password: L/Jlcu+tiIgvq9wEvnycxvEE3+RVixnY/YgUB/5mAdO1WLdlrt2CipYxGjnS/4A+NtR0TTldmItzY4UtbSRe6g==
second: #数据源2
url: jdbc:mysql://localhost2:3306/test2?allowMultiQueries=true&useUnicode=true&characterEncoding=UTF-8&useSSL=false
username: root
password: L/Jlcu+tiIgvq9wEvnycxvEE3+RVixnY/YgUB/5mAdO1WLdlrt2CipYxGjnS/4A+NtR0TTldmItzY4UtbSRe6g==
initial-size: 10
max-active: 100
min-idle: 10
max-wait: 60000
pool-prepared-statements: true
max-pool-prepared-statement-per-connection-size: 20
time-between-eviction-runs-millis: 60000
min-evictable-idle-time-millis: 300000
#validation-query: SELECT 1 FROM DUAL
test-while-idle: true
test-on-borrow: false
test-on-return: false
stat-view-servlet:
enabled: true
url-pattern: /druid/*
#login-username: admin
#login-password: admin
filter:
stat:
log-slow-sql: true
slow-sql-millis: 1000
merge-sql: false
wall:
config:
multi-statement-allow: true
从配置文件上看,有没有发现单数据源说要配置属性,多数据源竟然不用配置
spring.datasource.druid.connection-properties=config.decrypt=true;config.decrypt.key=${spring.datasource.publicKey}
spring.datasource.druid.filter.config.enabled=true
没配置的原因是,多数据源注入会在过滤器解密之前,这会导致数据源注入加密的密码,而由于没有解密,导致连不到数据库,因此配置了也没用,其次如果多个数据源的数据库密码不一样,产生的公钥都是不一样的,用原生提供的configFilter没办法进行解析,为啥这么说,后面源码解析会说。如果配置不行,那可以从代码层面上考虑
@Bean
@Primary
public DynamicDataSource dataSource(DataSource firstDataSource, DataSource secondDataSource) {
Map<Object, Object> targetDataSources = new HashMap<>();
targetDataSources.put(DataSourceNames.FIRST, this.getDataSourceWithDecryptPwd(firstDataSource));
targetDataSources.put(DataSourceNames.SECOND, this.getDataSourceWithDecryptPwd(secondDataSource));
return new DynamicDataSource(firstDataSource, targetDataSources);
}
private DataSource getDataSourceWithDecryptPwd(DataSource dataSource){
try {
if(dataSource instanceof DruidDataSource){
DruidDataSource druidDataSource = (DruidDataSource) dataSource;
String passwordPlainText = ConfigTools.decrypt(publicKey, druidDataSource.getPassword());
druidDataSource.setPassword(passwordPlainText);
return druidDataSource;
}
} catch (Exception e) {
logger.error("getDataSourceWithDecryptPwd error:"+e.getMessage(),e);
}
return dataSource;
}
其核心原理就是在多数据源注入之前,进行密码解密,解密的核心方法是由阿里提供工具类
com.alibaba.druid.filter.config.ConfigTools
之前我们单数据源提到为什么要开启configfilter,不然解密无法操作,我们看下这个类到底是做了啥
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 = this.loadPropertyFromConfigFile(connectionProperties);
boolean decrypt = this.isDecrypt(connectionProperties, configFileProperties);
if (configFileProperties == null) {
if (decrypt) {
this.decrypt(dataSource, (Properties)null);
}
} else {
if (decrypt) {
this.decrypt(dataSource, configFileProperties);
}
try {
DruidDataSourceFactory.config(dataSource, configFileProperties);
} catch (SQLException var7) {
throw new IllegalArgumentException("Config DataSource error.", var7);
}
}
}
public boolean isDecrypt(Properties connectionProperties, Properties configFileProperties) {
String decrypterId = connectionProperties.getProperty("config.decrypt");
if ((decrypterId == null || decrypterId.length() == 0) && configFileProperties != null) {
decrypterId = configFileProperties.getProperty("config.decrypt");
}
if (decrypterId == null || decrypterId.length() == 0) {
decrypterId = System.getProperty("druid.config.decrypt");
}
return Boolean.valueOf(decrypterId);
}
Properties loadPropertyFromConfigFile(Properties connectionProperties) {
String configFile = connectionProperties.getProperty("config.file");
if (configFile == null) {
configFile = System.getProperty("druid.config.file");
}
if (configFile != null && configFile.length() > 0) {
if (LOG.isInfoEnabled()) {
LOG.info("DruidDataSource Config File load from : " + configFile);
}
Properties info = this.loadConfig(configFile);
if (info == null) {
throw new IllegalArgumentException("Cannot load remote config file from the [config.file=" + configFile + "].");
} else {
return info;
}
} else {
return null;
}
}
public void decrypt(DruidDataSource dataSource, Properties info) {
try {
String encryptedPassword = null;
if (info != null) {
encryptedPassword = info.getProperty("password");
}
if (encryptedPassword == null || encryptedPassword.length() == 0) {
encryptedPassword = dataSource.getConnectProperties().getProperty("password");
}
if (encryptedPassword == null || encryptedPassword.length() == 0) {
encryptedPassword = dataSource.getPassword();
}
PublicKey publicKey = this.getPublicKey(dataSource.getConnectProperties(), info);
String passwordPlainText = ConfigTools.decrypt(publicKey, encryptedPassword);
if (info != null) {
info.setProperty("password", passwordPlainText);
} else {
dataSource.setPassword(passwordPlainText);
}
} catch (Exception var6) {
throw new IllegalArgumentException("Failed to decrypt.", var6);
}
}
public PublicKey getPublicKey(Properties connectionProperties, Properties configFileProperties) {
String key = null;
if (configFileProperties != null) {
key = configFileProperties.getProperty("config.decrypt.key");
}
if (StringUtils.isEmpty(key) && connectionProperties != null) {
key = connectionProperties.getProperty("config.decrypt.key");
}
if (StringUtils.isEmpty(key)) {
key = System.getProperty("druid.config.decrypt.key");
}
return ConfigTools.getPublicKey(key);
}
public Properties loadConfig(String filePath) {
Properties properties = new Properties();
InputStream inStream = null;
URL url;
try {
boolean xml = false;
if (filePath.startsWith("file://")) {
filePath = filePath.substring("file://".length());
inStream = this.getFileAsStream(filePath);
xml = filePath.endsWith(".xml");
} else if (!filePath.startsWith("http://") && !filePath.startsWith("https://")) {
if (filePath.startsWith("classpath:")) {
String resourcePath = filePath.substring("classpath:".length());
inStream = Thread.currentThread().getContextClassLoader().getResourceAsStream(resourcePath);
xml = resourcePath.endsWith(".xml");
} else {
inStream = this.getFileAsStream(filePath);
xml = filePath.endsWith(".xml");
}
} else {
url = new URL(filePath);
inStream = url.openStream();
xml = url.getPath().endsWith(".xml");
}
if (inStream == null) {
LOG.error("load config file error, file : " + filePath);
url = null;
return url;
}
if (xml) {
properties.loadFromXML(inStream);
} else {
properties.load(inStream);
}
Properties var12 = properties;
return var12;
} catch (Exception var9) {
LOG.error("load config file error, file : " + filePath, var9);
url = null;
} finally {
JdbcUtils.close(inStream);
}
return url;
}
private InputStream getFileAsStream(String filePath) throws FileNotFoundException {
InputStream inStream = null;
File file = new File(filePath);
if (file.exists()) {
inStream = new FileInputStream(file);
} else {
inStream = Thread.currentThread().getContextClassLoader().getResourceAsStream(filePath);
}
return (InputStream)inStream;
}
}
从源码上我们可以知道这个类主要做的事情,加载配置文件信息,解密,把解密后的密码重新设置进druid数据源中,其核心方法是decrypt,这个方法主要用来解密,这方法里面里面重点关注
PublicKey publicKey = this.getPublicKey(dataSource.getConnectProperties(), info);
String passwordPlainText = ConfigTools.decrypt(publicKey, encryptedPassword);
if (info != null) {
info.setProperty("password", passwordPlainText);
} else {
dataSource.setPassword(passwordPlainText);
}
看到没有,里面的方法片段有个解密关键
String passwordPlainText = ConfigTools.decrypt(publicKey, encryptedPassword);
现在来解答用原生提供的configFilter没办法进行解析,我们看下如下方法
PublicKey publicKey = this.getPublicKey(dataSource.getConnectProperties(), info);
public PublicKey getPublicKey(Properties connectionProperties, Properties configFileProperties) {
String key = null;
if (configFileProperties != null) {
key = configFileProperties.getProperty("config.decrypt.key");
}
if (StringUtils.isEmpty(key) && connectionProperties != null) {
key = connectionProperties.getProperty("config.decrypt.key");
}
if (StringUtils.isEmpty(key)) {
key = System.getProperty("druid.config.decrypt.key");
}
return ConfigTools.getPublicKey(key);
}
很显然传入多个publickey,会被它当成一个,因此要实现识别多个publickey就要额外自定过滤器进行扩展了
druid数据库密码加密原理上不会很难,其实不少开发正常都不会对数据库密码再进行加密,可能是出于性能考虑,没必要去实现这样一个看似鸡肋的功能,可能觉得因为平时项目都是部署在内网里面,就算密码被知道了,也没啥,写这篇文章主要因为很少有看到百度上有对多数据源druid数据库密码加密的讲解,可能是因为这个太简单了原因吧,哈哈哈哈哈哈