前言
最近接了一个外包单(基于springboot2,连接池为druid),客户经费有限,基本上要啥,啥没有,项目基本上是托管在私人的某gay,某云等,本着让客户放心的原则,就在安全方面多考虑了一点,首先比如数据库密码加密之类的,虽然要是有心要破解也是容易,但至少加密给自己心里一点暗示。。。废话有点多,进入正题,本文主要分为3个部分,第一部分是单个数据源密码加密,第二部分是多个数据源密码加密,第三部分是简要的解密源码分析。
单数据源密码加密
1、 下载druid.jar,可以从maven中央仓库上下载
https://mvnrepository.com/artifact/com.alibaba/druid
我这边下载的版本是如下
<dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.1.10</version> </dependency>
这个jar主要是用来生成加密密码
2、生成加密密码
利用刚才下好的jar,在cmd中执行如下命令
java -cp D:/xxxx/druid-1.2.8/druid-1.2.8.jar com.alibaba.druid.filter.config.ConfigTools root
java -cp D:/xxxx/druid-1.2.8/druid-1.2.8.jar com.alibaba.druid.filter.config.ConfigTools 123456
注:123456为你数据库的密码
对我们有用的是publicKey和加密后的password,这个publickey主要是用来解密的秘钥
3、修改springboot配置文件配置,参考如下
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: ENC(EChlZH6XdPa129Ii3akF/92alTAIifYXdbFJaqKlmcQNF0a/QRoaAook16eO72vw4S4Ut8nXeoGOMdgFkQuW+A==) 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}
3.1、配置文件跟加密相关的属性
spring.datasource.druid.connection-properties
这个属性配置的value是键值对,其中config.decrypt=true表示要进行解密,config.decrypt.key=${spring.datasource.publicKey}注入要解密需要的公钥
spring.datasource.druid.filter.config.enabled=true
开启configFilter, 这个不开启是没办法进行解密操作的
多数据源密码加密配置
1、对数据库密码进行加密
这个步骤和单数据源密码加密一样,就略过
2、修改springboot配置文件参考如下
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
config: enabled: true
connection-properties: config.decrypt=true;config.decrypt.key=${spring.datasource.publicKey}
从配置文件上看,有没有发现单数据源说要配置属性,多数据源竟然不用配置
spring.datasource.druid.connection-properties=config.decrypt=true;config.decrypt.key=${spring.datasource.publicKey}
spring.datasource.druid.filter.config.enabled=true
没配置的原因是,多数据源注入会在过滤器解密之前,这会导致数据源注入加密的密码,而由于没有解密,导致连不到数据库,因此配置了也没用,其次如果多个数据源的数据库密码不一样,产生的公钥都是不一样的,用原生提供的configFilter没办法进行解析,为啥这么说,后面源码解析会说。如果配置不行,那可以从代码层面上考虑
3、代码层进行多数据源密码解密,其代码如下
@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
druid数据库密码解密源码分析
之前我们单数据源提到为什么要开启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数据库密码加密的讲解,可能是因为这个太简单了原因吧