Springboot+mybatis-plus+dynamic-datasource+Druid数据库配置加密
背景
生产环境中, 为了保密,我们希望将数据库密码加密, 甚至用户名和jdbc连接串加密。本章我们使用由苞米豆(baomidou)团队开发的dynamic-datasource多数据源组件自带的加密工具实现数据库配置加密
从dynamic-datasource-starter官方给的特性说明中,我们可以看到 dynamic-datasource-starter 支持数据库敏感配置信息 加密(可自定义) ENC()。,所以我们撸一下源码看看到底怎么实现的。
经过查看源码发现, 该框架使用自带的加密工具类com.baomidou.dynamic.datasource.toolkit.CryptoUtils进行加解密, 且自带有公钥私钥。但是源码咋看都觉得眼熟,而且注释是@author alibaba,翻看druid 的加密方法 com.alibaba.druid.filter.config.ConfigTools 。原来作者直接借鉴了Druid加密方法,之前看druid 的加密的时候发现公钥和私钥的使用反了,wenshao给的解释是历史原因。历史背景和源码咱们就聊到这,所以直接使用此工具类加密我们的明文密码, 然后用ENC()包裹即可实现加解密。
在使用 Spring Boot、MyBatis-Plus、 Druid 进行数据库配置时,如果需要对敏感配置信息进行加密,可以通过以下方式实现:
这是基本的思路,本章我们不造轮子,直接使用国产优秀框架baomidou团队的工具 dynamic-datasource多数据源组件自带的加密工具实现数据库配置加密
import com.baomidou.dynamic.datasource.toolkit.CryptoUtils; public class DBCryptoUtils extends CryptoUtils{ // 第一种方式 使用默认key 加密解密 public static void test1( ) throws Exception { System.out.println("-------------------------------------默认加密-------------------------------------"); String password = "abc123"; String encodePassword = CryptoUtils.encrypt(password); System.out.println("加密后密码:"+CryptoUtils.encrypt(password)); } // 第二种方式 使用自定义key,强烈建议 public static void test2( ) throws Exception { System.out.println("-------------------------------------自定义key-------------------------------------"); String[] pair = CryptoUtils.genKeyPair(512); System.out.println("privateKey: " + pair[0]); System.out.println("publicKey: " + pair[1]); // 按道理应该用公钥加密,私钥解密,作者直接抄的druid 的,把这个不规范的写法也给抄过来了,不影响效果,只觉得怪怪的。 System.out.println("加密后密码: " + CryptoUtils.encrypt(pair[0], "abc123")); } public static void main(String[] args) throws Exception { test1(); test2(); } }
spring: datasource: dynamic: #有默认值可以不配置,强烈建议更换 public-key: datasource: master: url: DB地址 username: 用户名 #配置加密后的密码 password: ENC(Ue9QTmtvOX8XMdRIZVqUAbmbLNfAjQQO9jokfVEfaew+HFGZPndSmcq2pOTS2xuC7Pg/z1gUGS82HOmWw0d9Cw==) driver-class-name: com.mysql.jdbc.Driver# 驱动保持jdbc url一致 public-key: #在多数据源下每个数据源可以独立设置,没有就继承上面的。
spring: datasource: dynamic: # 配置上面输出的 public key public-key: MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAJirfs9pc4fsDdXqjMto4zY+sYZ7d/XYwIQIYqj2FoqxvVC61tjKtG12nMSlwgXbV+DNpWh9W76QjM2XCNYB6VUCAwEAAQ== datasource: master: url: DB地址 username: 用户名 #配置加密后的密码 根据上面生成的秘钥,加密后的密码 password: ENC(BSbigK5YuTXLOUDekSm3uU+h/n2/rIwa4DxQWPbfuhf9irwoakQy777AYHqJVz/WEG5BTFp4Ym+lguH3o+f4kQ==) driver-class-name: com.mysql.jdbc.Driver# 驱动保持jdbc url一致 public-key: #在多数据源下每个数据源可以独立设置,没有就继承上面的。
到这儿基本上就OK了,如果想知道源码是怎么执行的可以继续向下看
我们先来看EncDataSourceInitEvent 源码.我们可以看到EncDataSourceInitEvent类,实现了DataSourceInitEvent接口。该类用于在数据源初始化之前对数据源属性进行解密操作。
/** * 多数据源默认解密事件 * * @author TaoYu */ @Slf4j public class EncDataSourceInitEvent implements DataSourceInitEvent { /** * 加密正则 */ private static final Pattern ENC_PATTERN = Pattern.compile("^ENC\\((.*)\\)$"); @Override public void beforeCreate(DataSourceProperty dataSourceProperty) { String publicKey = dataSourceProperty.getPublicKey(); if (StringUtils.hasText(publicKey)) { dataSourceProperty.setUrl(decrypt(publicKey, dataSourceProperty.getUrl())); dataSourceProperty.setUsername(decrypt(publicKey, dataSourceProperty.getUsername())); dataSourceProperty.setPassword(decrypt(publicKey, dataSourceProperty.getPassword())); } } @Override public void afterCreate(DataSource dataSource) { } /** * 字符串解密 */ private String decrypt(String publicKey, String cipherText) { if (StringUtils.hasText(cipherText)) { Matcher matcher = ENC_PATTERN.matcher(cipherText); if (matcher.find()) { try { return CryptoUtils.decrypt(publicKey, matcher.group(1)); } catch (Exception e) { log.error("DynamicDataSourceProperties.decrypt error ", e); } } } return cipherText; } }
DataSourceInitEvent 接口 又是干什么用的呢,我们点进去可以看到
DataSourceInitEvent是 baomidou的dynamic.datasource组件 位于com.baomidou.dynamic.datasource.event包下定义的一个钩子接口,在创建连接池前和后可以搞事情。
总之,这段代码是一个事件接口,用于在多数据源连接池创建过程中执行一些自定义操作。
让我们逐行解析代码的功能:
定义了一个名为ENC_PATTERN的正则表达式模式,用于匹配加密字符串的格式。
实现了beforeCreate方法,该方法在数据源创建之前调用。它接收一个DataSourceProperty对象作为参数,表示数据源的属性。
在beforeCreate方法中,首先获取数据源属性中的公钥(publicKey)。如果公钥存在,表示需要进行解密操作。
接下来,使用公钥对数据源的URL、用户名和密码进行解密操作。调用decrypt方法对这些属性进行解密,并将解密后的值设置回dataSourceProperty对象中。
实现了afterCreate方法,该方法在数据源创建之后调用。在该方法中可以执行一些额外的操作,但是该方法在给定的代码中没有实现任何逻辑。
定义了一个私有方法decrypt,用于对加密字符串进行解密。该方法接收公钥和密文作为参数,并返回解密后的明文字符串。
在decrypt方法中,首先判断密文是否符合加密格式(通过ENC_PATTERN进行匹配)。如果匹配成功,就使用给定的公钥和密文调用CryptoUtils.decrypt方法进行解密操作。
总结起来,EncDataSourceInitEvent类实现了DataSourceInitEvent接口,用于在数据源初始化之前对数据源属性进行解密操作。它通过正则表达式匹配加密字符串的格式,并使用给定的公钥对密文进行解密。解密后的明文值将用于设置数据源的URL、用户名和密码属性。
到这儿我们可能,已经知道怎么自定义解密,无外乎就是实现DataSourceInitEvent 接口的beforeCreate方法然后自定义处理。但是我们自定义的这个实现类和官方默认的解密实现类优先级怎么搞呢,其实官方在DynamicDataSourceAutoConfiguration配置类中已经使用了@condition条件注解。满足优先使用我们自定义的加密的实现类了。我们只需要交给Spring 容器就OK.
/** * 自定义多数据源默认解密事件 */ @Slf4j @Configuration public class CustomeEncDataSourceInitEvent implements DataSourceInitEvent { /** * 加密正则 实现自定义的 例如BCC */ private static final Pattern ENC_PATTERN = Pattern.compile("^BCC\\((.*)\\)$"); @Override public void beforeCreate(DataSourceProperty dataSourceProperty) { // TODO 实现自定义的解密 String publicKey = dataSourceProperty.getPublicKey(); if (StringUtils.hasText(publicKey)) { dataSourceProperty.setUrl(decrypt(publicKey, dataSourceProperty.getUrl())); dataSourceProperty.setUsername(decrypt(publicKey, dataSourceProperty.getUsername())); dataSourceProperty.setPassword(decrypt(publicKey, dataSourceProperty.getPassword())); } } @Override public void afterCreate(DataSource dataSource) { } /** * 字符串解密 */ private String decrypt(String publicKey, String cipherText) { if (StringUtils.hasText(cipherText)) { Matcher matcher = ENC_PATTERN.matcher(cipherText); if (matcher.find()) { try { return CryptoUtils.decrypt(publicKey, matcher.group(1)); } catch (Exception e) { log.error("DynamicDataSourceProperties.decrypt error ", e); } } } return cipherText; } }