网上的坑
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