网上的坑
springboot使用Druid数据库加密链接方案,不建议采用网上的一篇文章《springboot结合Druid加密数据库密码遇到的坑!》介绍的方式来进行加密链接实现。以下方式兼容SpringBoot1和SpringBoot2.0及以上。本文章下文分享Druid源码后就知道为什么不建议采用该方式的原因了。
加密准备
首先使用CMD生成数据库加密字符串,该命令会产生三个值 privateKey=公钥、publicKey=密码加密后的结果、password=密码加密串
java -cp druid-1.0.28.jar com.alibaba.druid.filter.config.ConfigTools pcds123123456
使用Durid的工具类ConfigTools验证加密字符串
@Test
public void db_decrypt_test() throws Exception {
String publicKey = "MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAJggkaRJ+bqLMF6pefubEDLViboxYKGTdGe+78DziIta8Nv8crOA83M0tFG8y8CqHcFYIbG89q9zcnNvL+E2/CECAwEAAQ==";
String password = "AgDRyKJ81Ku3o0HSyalDgCTtGsWcKz3fC0iM5pLur2QJnIF+fKWKFZ6c6e36M06tF2uCadvS/EodWxmRDWwvIA==";
System.out.println(ConfigTools.decrypt(publicKey, password));
}
实现方案
SpringBoot集成Druid数据库链接加密的方式,推荐使用配置注入方式初始化DataSource。方法如下:
一、增加配置注入Bean
import java.sql.SQLException;
import javax.sql.DataSource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import com.alibaba.druid.pool.DruidDataSource;
@Configuration
@ConfigurationProperties(prefix = "spring.datasource")
public class DbConfig {
private static final Logger LOGGER = LoggerFactory.getLogger(DbConfig.class);
private String url;
private String driverClassName;
private String username;
private String password;
private Integer initialSize;
private Integer minIdle;
private Integer maxActive;
private Integer maxWait;
private Integer timeBetweenEvictionRunsMillis;
private Integer minEvictableIdleTimeMillis;
private String validationQuery;
private Boolean testWhileIdle;
private Boolean testOnBorrow;
private Boolean testOnReturn;
private Boolean poolPreparedStatements;
private Integer maxOpenPreparedStatements;
private Integer maxPoolPreparedStatementPerConnectionSize;
private String filters;
private String publicKey;
private String connectionProperties;
@Primary
@Bean(name = "dataSource")
public DataSource dataSource() {
DruidDataSource datasource = new DruidDataSource();
datasource.setUrl(url);
datasource.setUsername(username);
datasource.setPassword(password);
datasource.setDriverClassName(driverClassName);
datasource.setInitialSize(initialSize);
datasource.setMinIdle(minIdle);
datasource.setMaxActive(maxActive);
datasource.setMaxWait(maxWait);
datasource.setTimeBetweenEvictionRunsMillis(timeBetweenEvictionRunsMillis);
datasource.setMinEvictableIdleTimeMillis(minEvictableIdleTimeMillis);
datasource.setValidationQuery(validationQuery);
datasource.setTestWhileIdle(testWhileIdle);
datasource.setTestOnBorrow(testOnBorrow);
datasource.setTestOnReturn(testOnReturn);
datasource.setPoolPreparedStatements(poolPreparedStatements);
datasource.setMaxOpenPreparedStatements(maxOpenPreparedStatements);
datasource.setMaxPoolPreparedStatementPerConnectionSize(maxPoolPreparedStatementPerConnectionSize);
datasource.setConnectionProperties(connectionProperties.replace("${publicKey}", publicKey));
try {
datasource.setFilters(filters);
} catch (SQLException e) {
LOGGER.error("druid configuration initialization filter", e);
}
return datasource;
}
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
public String getDriverClassName() {
return driverClassName;
}
public void setDriverClassName(String driverClassName) {
this.driverClassName = driverClassName;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public Integer getInitialSize() {
return initialSize;
}
public void setInitialSize(Integer initialSize) {
this.initialSize = initialSize;
}
public Integer getMinIdle() {
return minIdle;
}
public void setMinIdle(Integer minIdle) {
this.minIdle = minIdle;
}
public Integer getMaxActive() {
return maxActive;
}
public void setMaxActive(Integer maxActive) {
this.maxActive = maxActive;
}
public Integer getMaxWait() {
return maxWait;
}
public void setMaxWait(Integer maxWait) {
this.maxWait = maxWait;
}
public Integer getTimeBetweenEvictionRunsMillis() {
return timeBetweenEvictionRunsMillis;
}
public void setTimeBetweenEvictionRunsMillis(Integer timeBetweenEvictionRunsMillis) {
this.timeBetweenEvictionRunsMillis = timeBetweenEvictionRunsMillis;
}
public Integer getMinEvictableIdleTimeMillis() {
return minEvictableIdleTimeMillis;
}
public void setMinEvictableIdleTimeMillis(Integer minEvictableIdleTimeMillis) {
this.minEvictableIdleTimeMillis = minEvictableIdleTimeMillis;
}
public String getValidationQuery() {
return validationQuery;
}
public void setValidationQuery(String validationQuery) {
this.validationQuery = validationQuery;
}
public Boolean getTestWhileIdle() {
return testWhileIdle;
}
public void setTestWhileIdle(Boolean testWhileIdle) {
this.testWhileIdle = testWhileIdle;
}
public Boolean getTestOnBorrow() {
return testOnBorrow;
}
public void setTestOnBorrow(Boolean testOnBorrow) {
this.testOnBorrow = testOnBorrow;
}
public Boolean getTestOnReturn() {
return testOnReturn;
}
public void setTestOnReturn(Boolean testOnReturn) {
this.testOnReturn = testOnReturn;
}
public Boolean getPoolPreparedStatements() {
return poolPreparedStatements;
}
public void setPoolPreparedStatements(Boolean poolPreparedStatements) {
this.poolPreparedStatements = poolPreparedStatements;
}
public Integer getMaxOpenPreparedStatements() {
return maxOpenPreparedStatements;
}
public void setMaxOpenPreparedStatements(Integer maxOpenPreparedStatements) {
this.maxOpenPreparedStatements = maxOpenPreparedStatements;
}
public Integer getMaxPoolPreparedStatementPerConnectionSize() {
return maxPoolPreparedStatementPerConnectionSize;
}
public void setMaxPoolPreparedStatementPerConnectionSize(Integer maxPoolPreparedStatementPerConnectionSize) {
this.maxPoolPreparedStatementPerConnectionSize = maxPoolPreparedStatementPerConnectionSize;
}
public String getFilters() {
return filters;
}
public void setFilters(String filters) {
this.filters = filters;
}
public String getPublicKey() {
return publicKey;
}
public void setPublicKey(String publicKey) {
this.publicKey = publicKey;
}
public String getConnectionProperties() {
return connectionProperties;
}
public void setConnectionProperties(String connectionProperties) {
this.connectionProperties = connectionProperties;
}
}
二、增加配置文件
# mysql config
spring.datasource.name=pcdsdata
spring.datasource.url=jdbc:mysql://192.168.1.1/mydb
spring.datasource.username=root
spring.datasource.password=AgDRyKJ81Ku3o0HSyalDgCTtGsWcKz3fC0iM5pLur2QJnIF+fKWKFZ6c6e36M06tF2uCadvS/EodWxmRDWwvIA==
# druid datasource config
spring.datasource.driverClassName=com.mysql.jdbc.Driver
spring.datasource.maxActive=20
spring.datasource.initialSize=1
spring.datasource.minIdle=1
spring.datasource.maxWait=60000
spring.datasource.timeBetweenEvictionRunsMillis=60000
spring.datasource.minEvictableIdleTimeMillis=300000
spring.datasource.validationQuery=select 1 from dual
spring.datasource.testWhileIdle=true
spring.datasource.testOnBorrow=false
spring.datasource.testOnReturn=false
spring.datasource.poolPreparedStatements=true
spring.datasource.maxOpenPreparedStatements=20
spring.datasource.maxPoolPreparedStatementPerConnectionSize=20
spring.datasource.filters=config,stat,wall,log4j
spring.datasource.publicKey=MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAJggkaRJ+bqLMF6pefubEDLViboxYKGTdGe+78DziIta8Nv8crOA83M0tFG8y8CqHcFYIbG89q9zcnNvL+E2/CECAwEAAQ==
spring.datasource.connectionProperties=config.decrypt=true;config.decrypt.key=${spring.datasource.publicKey}
注意事项
1)config.decrypt=true;config.decrypt.key= 该两项配置为Druid内部固定Key。网上很多案例配置为publicKey,这是有问题的。
2)其他教程会配置DbType为 spring.datasource.type=com.alibaba.druid.pool.DruidDataSource 实际在Druid源码中会判断DbType为 MySql 或者Db2等类型。可删除此项 原逻辑将从 this.dbType = JdbcUtils.getDbType(jdbcUrl, null);//数据库链接字符串 获取DbType值
经过以上配置,SpringBoot使用Druid加密链接完成。如果过程中遇到其他问题可在下方留言,方便更多人解决类似问题。
经测试以上配置兼容SpringBoot2.0以上2.0.3/2.06等. 升级测试时需要注意jar包版本问题:
1)Gson升级到最新版本,如gson-2.8.5.jar. 否则报错 com.google.gson.GsonBuilder.setLenient()Lcom/google/gson/GsonBuilder; but it does not exist. Its class, com.google.gson.GsonBuilder, is available from the following locations:
2)log4j升级到log4j-1.2.17以上, 否则报错 Caused by: java.lang.ClassNotFoundException: org.apache.log4j.Priority
源码分析
1) 根据源码逻辑,如果不自定义注入Bean, 在SpringBoot初始化时只会读取系统配置参数,如下
public DruidDataSource(){
this(false);
}
public DruidDataSource(boolean fairLock){
super(fairLock);
configFromPropety(System.getProperties());
}
public void configFromPropety(Properties properties) {
{
Boolean value = getBoolean(properties, "druid.testWhileIdle");
if (value != null) {
this.setTestWhileIdle(value);
}
}
{
Boolean value = getBoolean(properties, "druid.testOnBorrow");
if (value != null) {
this.setTestOnBorrow(value);
}
}
...
2) 所有的配置在类DruidAbstractDataSource中,自定义注入Bean,将在这个类中设置相关属性
3) 在首次初始化数据库链接时将调用DruidDataSource.init(),并进入到ConfigFilter.init ,初始化建立链接。根据配置config.decrypt决定是否要进行解密动作,如需解密则加载config.decrypt.key和password(首先加载connectionProperties链接字符串中的password,没有再加载默认的spring.datasource.password) ConfigFilter.decrypt执行解密动作。
4) 文章《springboot结合Druid加密数据库密码遇到的坑!》中其实是绕过了是否要进行加密等配置,自己实现了解密动作再把数据库链接密码替换掉,这违背了Druid设计的初衷了。
参考资料
参考资料 https://github.com/alibaba/druid/wiki

