Springboot+dynamic-datasource+Druid数据库配置加密

Springboot+mybatis-plus+dynamic-datasource+Druid数据库配置加密

文章目录

  • 0.前言
  • 1. 动态添加移除数据源
  • 2.基础介绍
  • 3. 使用步骤示例
    • 简单方式,使用默认的加密
      • 1. 使用下面 工具类输出,加密后的密码
      • 1. 将上面加密后的密码配置到配置文件中
        • 如果使用的默认key,即上面生成加密后密码的第一种,则使用下面方式配置
        • 如果使用的自定义的key,即上面既生成publicKey和privateKey 以及加密后密码的第2种方式,则使用下面方式配置
        • 4. 官方源码分析
          • 5.1. 解密的核心源码
          • 5.2. 自定义解密
          • 5. 参考资料

            0.前言

            背景

            生产环境中, 为了保密,我们希望将数据库密码加密, 甚至用户名和jdbc连接串加密。本章我们使用由苞米豆(baomidou)团队开发的dynamic-datasource多数据源组件自带的加密工具实现数据库配置加密

            1. 动态添加移除数据源

            从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()包裹即可实现加解密。

            2.基础介绍

            在使用 Spring Boot、MyBatis-Plus、 Druid 进行数据库配置时,如果需要对敏感配置信息进行加密,可以通过以下方式实现:

            1. 使用加密算法对敏感信息进行加密,如数据库的用户名、密码等信息。可以选择对称加密算法(如AES)或非对称加密算法(如RSA)进行加密。
            2. 将加密后的敏感信息保存在安全的位置,如配置文件或系统环境变量。
            3. 在应用启动时,通过配置文件或环境变量读取加密后的敏感信息。
            4. 在配置动态数据源时,使用解密的敏感信息进行数据库配置。

            这是基本的思路,本章我们不造轮子,直接使用国产优秀框架baomidou团队的工具 dynamic-datasource多数据源组件自带的加密工具实现数据库配置加密

            3. 使用步骤示例

            简单方式,使用默认的加密

            1. 使用下面 工具类输出,加密后的密码

            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();
                    }
            }
            

            1. 将上面加密后的密码配置到配置文件中

            如果使用的默认key,即上面生成加密后密码的第一种,则使用下面方式配置
            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: #在多数据源下每个数据源可以独立设置,没有就继承上面的。
            
            如果使用的自定义的key,即上面既生成publicKey和privateKey 以及加密后密码的第2种方式,则使用下面方式配置
            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了,如果想知道源码是怎么执行的可以继续向下看

            4. 官方源码分析

            5.1. 解密的核心源码

            我们先来看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包下定义的一个钩子接口,在创建连接池前和后可以搞事情。

            1. 该接口定义了两个方法:beforeCreate和afterCreate,用于在连接池创建前后执行一些操作。
            2. beforeCreate方法接受一个DataSourceProperty参数,用于传递数据源的基本信息。
            3. afterCreate方法接受一个DataSource参数,表示已创建的连接池对象。

            总之,这段代码是一个事件接口,用于在多数据源连接池创建过程中执行一些自定义操作。

            让我们逐行解析代码的功能:

            1. 定义了一个名为ENC_PATTERN的正则表达式模式,用于匹配加密字符串的格式。

            2. 实现了beforeCreate方法,该方法在数据源创建之前调用。它接收一个DataSourceProperty对象作为参数,表示数据源的属性。

            3. 在beforeCreate方法中,首先获取数据源属性中的公钥(publicKey)。如果公钥存在,表示需要进行解密操作。

            4. 接下来,使用公钥对数据源的URL、用户名和密码进行解密操作。调用decrypt方法对这些属性进行解密,并将解密后的值设置回dataSourceProperty对象中。

            5. 实现了afterCreate方法,该方法在数据源创建之后调用。在该方法中可以执行一些额外的操作,但是该方法在给定的代码中没有实现任何逻辑。

            6. 定义了一个私有方法decrypt,用于对加密字符串进行解密。该方法接收公钥和密文作为参数,并返回解密后的明文字符串。

            7. 在decrypt方法中,首先判断密文是否符合加密格式(通过ENC_PATTERN进行匹配)。如果匹配成功,就使用给定的公钥和密文调用CryptoUtils.decrypt方法进行解密操作。

            总结起来,EncDataSourceInitEvent类实现了DataSourceInitEvent接口,用于在数据源初始化之前对数据源属性进行解密操作。它通过正则表达式匹配加密字符串的格式,并使用给定的公钥对密文进行解密。解密后的明文值将用于设置数据源的URL、用户名和密码属性。

            5.2. 自定义解密

            到这儿我们可能,已经知道怎么自定义解密,无外乎就是实现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;
                }
            }
            

            5. 参考资料

            1. dynamic-datasource GitHub 仓库 ↗:dynamic-datasource 的官方 GitHub 仓库,包含源代码、文档和示例等资源。