一、导入Druid依赖

<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid-spring-boot-starter</artifactId>
    <version>1.1.22</version>
</dependency>

二、生成密文

方式1. 找到存放druid jar包的目录

1-1、在目录栏执行cmd,调出命令行窗口

mysql启用密码插件 mysql启动密码_mysql

1-2、在命令行窗口执行
java -cp druid-1.1.22.jar com.alibaba.druid.filter.config.ConfigTools 数据库密码

## 如下
E:\java\.m2\repository\com\alibaba\druid\1.1.22> java -cp .\druid-1.1.22.jar com.alibaba.druid.filter.config.ConfigTools ltkj.test
privateKey:MIIBVAIBADANBgkqhkiG9w0BAQEFAASCAT4wggE6AgEAAkEAsQxrpzOi7BscR8nc5YIxRS8XUk3vN92V1wMUqu5EOKKr9D9MGVCZ3+7REzTB2uHUbRZgd4oCu7XwuLAx3GdOvwIDAQABAkAVDS2FuYGPQl/39zwhO/xAxisnOaYOhfXplW+xLViGRTqC4dTQ2h2LVMGpYhOPXCMNeMAO6ZhH4mSqNNMs16ClAiEAwaV1H8gjwFAfpMNhX76YjhtNkyt9dc8A2NbCqYvi6rMCIQDqDshX465wOSbnpXacW/8Uwi2Ku727YNFDKXJ9I6oBxQIgbjf0wFA0OSPhvvAOHmbnggr8ToX0dPeLreAfEE20rI0CIQCLSZrOOu9/V3OgnSZV7KWDW/8wNYO2s+o0tsCsWgH9JQIgPzjgmBX8s5CZwzxd2JIxAjH761CdNhTAmZjPPYuNLJg=
publicKey:MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBALEMa6czouwbHEfJ3OWCMUUvF1JN7zfdldcDFKruRDiiq/Q/TBlQmd/u0RM0wdrh1G0WYHeKAru18LiwMdxnTr8CAwEAAQ==
password:omjlkBmUL7vXvuiOUEsYBNejb9j1GuD7aCQWxWLSR9LohB5H7M+X8LR63wBUeyws7yRPNq/zejMAsZ82DX3z1A==

PS E:\java\.m2\repository\com\alibaba\druid\1.1.22>

复制password和publickey

方式2. 工具类生成
利用工具类生成加密后密码和公钥
public static void main(String[] args) throws Exception {
        String password = "root123";
        System.out.println("明文密码: " + password);
        String[] keyPair = ConfigTools.genKeyPair(512);
        //私钥
        String privateKey = keyPair[0];
        //公钥
        String publicKey = keyPair[1];
        //用私钥加密后的密文
        password = ConfigTools.encrypt(privateKey, password);

        System.out.println("privateKey:" + privateKey);
        System.out.println("publicKey:" + publicKey);

        System.out.println("password:" + password);

        String decryptPassword = ConfigTools.decrypt(publicKey, password);
        System.out.println("解密后:" + decryptPassword);
    }

三、项目配置

修改yml配置文件

1. filters添加config
2. 配置解密,同时指定公钥

2-1. 生产环境 建议如下配置
启动时,通过java -jar 启动命令,指定spring.druid.publickey的值,如(java -jar xx.jar --spring.druid.publickey=公钥),
避免通过yml获取到公钥。
2-2 . 开发环境可将公钥直接配置在idea启动参数内。

connectionProperties: config.decrypt=true;config.decrypt.key=${spring.druid.publickey}
datasource:
    username: root
    password: RuacV1QzH80HVwZpR5MqagLoOWbRPYPj+yXKJrfEXJxIVchnWfGpdi2PTJCAlWoi7hNN+y4hhDmiGEvdYscW4w==
    url: jdbc:mysql://localhost:3306/article?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
    driver-class-name: com.mysql.jdbc.Driver
    type: com.alibaba.druid.pool.DruidDataSource
    #druid 数据源专有配置
    druid:
      #配置druid监控页面
      stat-view-servlet:
        enabled: true
        url-pattern: /druid/*
        loginUsername: admin
        loginPassword: 123456
    initialSize: 5
    minIdle: 5
    maxActive: 20
    maxWait: 60000
    timeBetweenEvictionRunsMillis: 60000
    minEvictableIdleTimeMillis: 300000
    validationQuery: SELECT 1 FROM DUAL
    testWhileIdle: true
    testOnBorrow: false
    testOnReturn: false
    # 打开PSCache
    poolPreparedStatements: true
    #配置监控统计拦截的filters,stat:监控统计、log4j:日志记录、wall:防御sql注入
    #如果运行时报错  java.lang.ClassNotFoundException: org.apache.log4j.Priority
    #则导入 log4j 依赖即可,Maven 地址: https://mvnrepository.com/artifact/log4j/log4jDruidDataSourceFactory
    filters: stat,wall,config
    #指定每个连接上PSCache的大小
    maxPoolPreparedStatementPerConnectionSize: 20
    #合并多个DruidDataSource的监控数据
    useGlobalDataSourceStat: true
    #通过connectProperties属性来打开mergeSql功能;慢SQL记录;配置数据库密码解密;
    connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=2000;config.decrypt=true;config.decrypt.key=${spring.druid.publickey}

Note: 需要注意的是

druid是用私钥加密,公钥解密的,即如果公钥为所有人可见,那么所有人均可以解密。因此,上述配置若在团队内可见,没什么影响,但是如果配置文件万一被放到了公网上,相当于把钥匙插在锁上,白加密了。。在生产环境下,建议改为如下配置:

--- # 临时文件存储位置 避免临时文件被系统清理报错
spring.servlet.multipart.location: /ltkj/server/temp

--- # 监控中心配置
spring:
  boot:
    admin:
      client:
        # 增加客户端开关
        enabled: true
        url: http://localhost:9094/admin
        username: ltkj
        password: 123456
        # 将client的用户名密码发送至server
        instance:
          service-host-type: IP
          metadata:
            user.name: ltkj
            user.password: 123456
  servlet:
    multipart:
      max-file-size: 5000MB
      max-request-size: 5000MB

--- # xxl-job 配置
xxl.job:
  # 执行器开关
  enabled: false
  # 调度中心地址:如调度中心集群部署存在多个地址则用逗号分隔。
  admin-addresses: http://localhost:9200/xxl-job-admin
  # 执行器通讯TOKEN:非空时启用
  access-token: xxl-job
  executor:
    # 执行器AppName:执行器心跳注册分组依据;为空则关闭自动注册
    appname: xxl-job-executor
    # 执行器端口号 执行器从9101开始往后写
    port: 9101
    # 执行器注册:默认IP:PORT
    address:
    # 执行器IP:默认自动获取IP
    ip: 127.0.0.1
    # 执行器运行日志文件存储磁盘路径
    logpath: ./logs/xxl-job
    # 执行器日志文件保存天数:大于3生效
    logretentiondays: 30

--- # 数据源配置
spring:
  datasource:
    #    type: com.zaxxer.hikari.HikariDataSource
    type: com.alibaba.druid.pool.DruidDataSource
    # 动态数据源文档 https://www.kancloud.cn/tracy5546/dynamic-datasource/content
    dynamic:
      # 性能分析插件(有性能损耗 不建议生产环境使用)
      p6spy: false
      # 设置默认的数据源或者数据源组,默认值即为 master
      primary: master
      # 严格模式 匹配不到数据源则报错
      strict: true
      datasource:
        # 主库数据源
        master:
          type: ${spring.datasource.type}
          driverClassName: com.mysql.cj.jdbc.Driver
          # jdbc 所有参数配置参考 
          # rewriteBatchedStatements=true 批处理优化 大幅提升批量插入更新删除性能(对数据库有性能损耗 使用批量操作应考虑性能问题)
          url: jdbc:mysql://192.168.234.128:3306/ltkj-personnel-manager?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&autoReconnect=true&rewriteBatchedStatements=true&nullCatalogMeansCurrent=true
          username: ltkjtest
          password: omjlkBmUL7vXvuiOUEsYBNejb9j1GuD7aCQWxWLSR9LohB5H7M+X8LR63wBUeyws7yRPNq/zejMAsZ82DX3z1A==
          druid:
            filters: config
            connection-properties: config.decrypt=true
            public-key: ${ltkj.publickey}

        #          url: jdbc:mysql://192.168.75.131:3306/ltkj-personnel-manager?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&autoReconnect=true&rewriteBatchedStatements=true&nullCatalogMeansCurrent=true
        #          username: root
        #          password: root
        # 从库数据源
        slave:
          lazy: true
          type: ${spring.datasource.type}
          driverClassName: com.mysql.cj.jdbc.Driver
          url: jdbc:mysql://192.168.234.128:3306/ltkj-personnel-manager?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&autoReconnect=true&rewriteBatchedStatements=true&nullCatalogMeansCurrent=true
          username: ltkjtest
          password: omjlkBmUL7vXvuiOUEsYBNejb9j1GuD7aCQWxWLSR9LohB5H7M+X8LR63wBUeyws7yRPNq/zejMAsZ82DX3z1A==
          druid:
            filters: config
            connection-properties: config.decrypt=true
            public-key: ${spring.datasource.druid.public-key}

#          url: jdbc:mysql://192.168.75.131:3306/ltkj-personnel-manager?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&autoReconnect=true&rewriteBatchedStatements=true&nullCatalogMeansCurrent=true
#          username: root
#          password: root

--- # redis 单机配置(单机与集群只能开启一个另一个需要注释掉)
spring:
  redis:
    # 地址
#    host: 192.168.0.225
    host: localhost
    # 端口,默认为6379
    port: 6379
    # 数据库索引
    database: 2
    # 密码(如没有密码请注释掉)
    password: ltkj.test
    # 连接超时时间
    timeout: 10s
    # 是否开启ssl
    ssl: false

redisson:
  # redis key前缀
  keyPrefix:
  # 线程池数量
  threads: 16
  # Netty线程池数量
  nettyThreads: 32
  # 单节点配置
  singleServerConfig:
    # 客户端名称
    clientName: ${ltkj.name}
    # 最小空闲连接数
    connectionMinimumIdleSize: 32
    # 连接池大小
    connectionPoolSize: 64
    # 连接空闲超时,单位:毫秒
    idleConnectionTimeout: 10000
    # 命令等待超时,单位:毫秒
    timeout: 3000
    # 发布和订阅连接池大小
    subscriptionConnectionPoolSize: 50

--- # mail 邮件发送
mail:
  enabled: false
  host: smtp.163.com
  port: 465
  # 是否需要用户名密码验证
  auth: true
  # 发送方,遵循RFC-822标准
  from: xxx@163.com
  # 用户名(注意:如果使用foxmail邮箱,此处user为qq号)
  user: xxx@163.com
  # 密码(注意,某些邮箱需要为SMTP服务单独设置密码,详情查看相关帮助)
  pass: xxxxxxxxxx
  # 使用 STARTTLS安全连接,STARTTLS是对纯文本通信协议的扩展。
  starttlsEnable: true
  # 使用SSL安全连接
  sslEnable: true
  # SMTP超时时长,单位毫秒,缺省值不超时
  timeout: 0
  # Socket连接超时值,单位毫秒,缺省值不超时
  connectionTimeout: 10

--- # sms 短信
sms:
  enabled: false
  # 阿里云 dysmsapi.aliyuncs.com
  # 腾讯云 sms.tencentcloudapi.com
  endpoint: "dysmsapi.aliyuncs.com"
  accessKeyId: xxxxxxx
  accessKeySecret: xxxxxx
  signName: 测试
  # 腾讯专用
  sdkAppId:

#file
file:
  path: /data/wwwroot/static.ltkj.com/personnel
#  host: https://static.ltkj.com
  host: http://192.168.0.225:9002

一行之差:

config.decrypt.key: ${spring.datasource.druid.publickey},

这里${spring.datasource.druid.publickey}可以有两种传入方式:

通过服务启动参数

java -jar xxx.jar --spring.datasource.druid.publickey=
MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAIYYdcdptMU6n/4wtb7StmX4LFvmlw7+b5KHm7L8C0txn1+iMeXz3FM7emkGkKMuaLd9OazkjgxNpPCDRaNM7ecCAwEAAQ==

通过系统环境变量
这个可以通过Idea IDE传入(开发环境),或者实际的系统环境变量传入(生产环境)。

mysql启用密码插件 mysql启动密码_mysql启用密码插件_02

总结

实现对配置文件敏感数据的加密,方式很多,但一定要注意安全性,不可以把公钥公开配置在文件中;还是开头那句话:

一把插着钥匙的锁,能说它是安全的吗?

测试环境的问题:

convert java.lang.String to java.lang.Class<javax.sql.DataSource

如果 遇到上面的问题,因为 druid-spring-boot-starter 包是新加的, 没有上传到测试环境的 lib/ 下,

  • 清空 lib/ ,再rz全部 上传依赖包

mysql启用密码插件 mysql启动密码_spring_03

mysql启用密码插件 mysql启动密码_mysql_04

四、ConfigFilter解密原理分析

对于上面为何通过dataSource.setFilters(“config”)一行代码就能实现数据库密码的解密功能,你心中是否有疑惑,它具体又是如何配置了一个ConfigFilter实例的呢?带着这个疑问,我们看下DruidDataSource类中两个重要的方法入手:setFilters和setConnectionproperties,通过这两个入口方法找到与数据库连接密码解密有关的源码实现

3.1 DruidDataSource#setFilters方法

public void setFilters(String filters) throws SQLException {
        if (filters != null && filters.startsWith("!")) {
            filters = filters.substring(1);
            this.clearFilters();
        }
        this.addFilters(filters);
    }

    public void addFilters(String filters) throws SQLException {
        if (filters == null || filters.length() == 0) {
            return;
        }
        // 多个filter通过逗号分隔
        String[] filterArray = filters.split("\\,");

        for (String item : filterArray) {
            FilterManager.loadFilter(this.filters, item.trim());
        }
    }

在上面的addFilters方法中会去遍历配置的filter数组并调用FilterManager#loadFilter方法加载过滤器

3.2 FilterManager类静态代码块
而在FilterManager类中有这样一段静态代码

static {
        try {
            Properties filterProperties = loadFilterConfig();
            for (Map.Entry<Object, Object> entry : filterProperties.entrySet()) {
                String key = (String) entry.getKey();
                if (key.startsWith("druid.filters.")) {
                    String name = key.substring("druid.filters.".length());
                    aliasMap.put(name, (String) entry.getValue());
                }
            }
        } catch (Throwable e) {
            LOG.error("load filter config error", e);
        }
    }

上面这段静态代码首先会去调用无参的loadFilterConfig方法加载过滤器配置

public static Properties loadFilterConfig() throws IOException {
        Properties filterProperties = new Properties();

        loadFilterConfig(filterProperties, ClassLoader.getSystemClassLoader());
        loadFilterConfig(filterProperties, FilterManager.class.getClassLoader());
        loadFilterConfig(filterProperties, Thread.currentThread().getContextClassLoader());

        return filterProperties;
    }

而上面的静态方法中又会去调用带两个参数的loadFilterConfig方法,加载druid.jar包中类路径下的META-INF/druid-filter.properties属性配置文件

我们来看下druid-filter.properties文件中有哪些过滤器

druid.filters.default=com.alibaba.druid.filter.stat.StatFilter
druid.filters.stat=com.alibaba.druid.filter.stat.StatFilter
druid.filters.mergeStat=com.alibaba.druid.filter.stat.MergeStatFilter
druid.filters.counter=com.alibaba.druid.filter.stat.StatFilter
druid.filters.encoding=com.alibaba.druid.filter.encoding.EncodingConvertFilter
druid.filters.log4j=com.alibaba.druid.filter.logging.Log4jFilter
druid.filters.log4j2=com.alibaba.druid.filter.logging.Log4j2Filter
druid.filters.slf4j=com.alibaba.druid.filter.logging.Slf4jLogFilter
druid.filters.commonlogging=com.alibaba.druid.filter.logging.CommonsLogFilter
druid.filters.commonLogging=com.alibaba.druid.filter.logging.CommonsLogFilter
druid.filters.wall=com.alibaba.druid.wall.WallFilter
druid.filters.config=com.alibaba.druid.filter.config.ConfigFilter
druid.filters.haRandomValidator=com.alibaba.druid.pool.ha.selector.RandomDataSourceValidateFilter

可以看到总共有13个过滤器,ConfigFilter类对应的key为druid.filters.config
然后我们回到最上面的静态代码块中可以看到程序会遍历加载并读取druid-filter.properties文件中属性变量后返回的filterProperties, 并将其中的key截取掉druid.filters.前缀后的字符串作为name和过滤器的全类名作为键值对保存在ConcurrentHashMap<String, String>数据结构的aliasMap属性中。

3.3 FilterManager#loadFilter方法
————————————————

public static void loadFilter(List<Filter> filters, String filterName) throws SQLException {
        if (filterName.length() == 0) {
            return;
        }

        String filterClassNames = getFilter(filterName);

        if (filterClassNames != null) {
            for (String filterClassName : filterClassNames.split(",")) {
                if (existsFilter(filters, filterClassName)) {
                    continue;
                }

                Class<?> filterClass = Utils.loadClass(filterClassName);

                if (filterClass == null) {
                    LOG.error("load filter error, filter not found : " + filterClassName);
                    continue;
                }

                Filter filter;

                try {
                    filter = (Filter) filterClass.newInstance();
                } catch (ClassCastException e) {
                    LOG.error("load filter error.", e);
                    continue;
                } catch (InstantiationException e) {
                    thrownew SQLException("load managed jdbc driver event listener error. " + filterName, e);
                } catch (IllegalAccessException e) {
                    thrownew SQLException("load managed jdbc driver event listener error. " + filterName, e);
                } catch (RuntimeException e) {
                    thrownew SQLException("load managed jdbc driver event listener error. " + filterName, e);
                }

                filters.add(filter);
            }

            return;
        }

        if (existsFilter(filters, filterName)) {
            return;
        }

        Class<?> filterClass = Utils.loadClass(filterName);
        if (filterClass == null) {
            LOG.error("load filter error, filter not found : " + filterName);
            return;
        }

        try {
            Filter filter = (Filter) filterClass.newInstance();
            filters.add(filter);
        } catch (Exception e) {
            thrownew SQLException("load managed jdbc driver event listener error. " + filterName, e);
        }
    }
————————————————

上面这个方法的目的就是去根据配置的filterName去aliasMap中找到全类名,然后使用类加载器根据filter的全类名加载Filter类并实例化,完成实例化后将Filter类实例添加到DruidDataSource类List数据结构的filters属性中;当然这个过程首先会去判断filters中是否已经有了配置的Filter类实例,有的化则无需再次加载和实例化。

3.4 数据库连接密文解密的具体实现
在ConfigFilter类中有个init方法,正是在这个初始化方法中完成了数据源加密密码的解密

public void init(DataSourceProxy dataSourceProxy) {
      // 传入的dataSourceProxy就是我们的DruidDatasource实例  
     if (!(dataSourceProxy instanceof DruidDataSource)) {
            LOG.error("ConfigLoader only support DruidDataSource");
        }
        // DruidDataSource 转 DruidDataSource
        DruidDataSource dataSource = (DruidDataSource) dataSourceProxy;
        // 获取数据源中的连接属性
        Properties connectionProperties = dataSource.getConnectProperties();
        // 加载连接属性中配置的加密属性文件
        Properties configFileProperties = loadPropertyFromConfigFile(connectionProperties);

        // 判断是否需要解密,如果需要就进行解密
        boolean decrypt = isDecrypt(connectionProperties, configFileProperties);

        if (configFileProperties == null) {
            if (decrypt) {
                decrypt(dataSource, null);
            }
            return;
        }
        if (decrypt) {
            decrypt(dataSource, configFileProperties);
        }

        try {
            DruidDataSourceFactory.config(dataSource, configFileProperties);
        } catch (SQLException e) {
            thrownew IllegalArgumentException("Config DataSource error.", e);
        }
    }
1

上面这个ConfigFilter#init方法是在DruidDatasource#init方法中触发的

for (Filter filter : filters) {
       filter.init(this);
}

loadPropertyFromConfigFile方法源码

publics tatic final String CONFIG_FILE = "config.file";
public static final String SYS_PROP_CONFIG_FILE = "druid.config.file";

Properties loadPropertyFromConfigFile(Properties connectionProperties) {
        String configFile = connectionProperties.getProperty(CONFIG_FILE);

        if (configFile == null) {
            configFile = System.getProperty(SYS_PROP_CONFIG_FILE);
        }

        if (configFile != null && configFile.length() > 0) {
            if (LOG.isInfoEnabled()) {
                LOG.info("DruidDataSource Config File load from : " + configFile);
            }

            Properties info = loadConfig(configFile);

            if (info == null) {
                thrownew IllegalArgumentException("Cannot load remote config file from the [config.file=" + configFile
                                                   + "].");
            }

            return info;
        }

        return null;
    }

阅读loadPropertyFromConfigFile方法中的源码可见,加密属性文件主要从连接属性中key为config.file的属性文件位置或系统属性中key为druid.config.file映射的加密属性文件位置加载

isDecrypt方法源码

public static final String CONFIG_DECRYPT = "config.decrypt";
public static final String SYS_PROP_CONFIG_DECRYPT = "druid.config.decrypt";
public boolean isDecrypt(Properties connectionProperties, Properties configFileProperties) {
        String decrypterId = connectionProperties.getProperty(CONFIG_DECRYPT);
        if (decrypterId == null || decrypterId.length() == 0) {
            if (configFileProperties != null) {
                decrypterId = configFileProperties.getProperty(CONFIG_DECRYPT);
            }
        }
        if (decrypterId == null || decrypterId.length() == 0) {
            decrypterId = System.getProperty(SYS_PROP_CONFIG_DECRYPT);
        }
        return Boolean.valueOf(decrypterId);
    }

由isDecrypt方法中源码分析可见判断是否需要解密主要看连接属性或者加载的加密属性文件变量中key为config.decrypt的值是否为true;如果以上两个的值都不存在,则继续判断系统属性key为druid.config.decrypt的值是否为true

decrypt方法源码分析

public void decrypt(DruidDataSource dataSource, Properties info) {

        try {
            String encryptedPassword = null;
            // 若连接属性不为空,则从连接属性中获取加密密码,否则从数据源实例中获取加密密码
            if (info != null) {
                encryptedPassword = info.getProperty(DruidDataSourceFactory.PROP_PASSWORD);
            }

            if (encryptedPassword == null || encryptedPassword.length() == 0) {
                encryptedPassword = dataSource.getConnectProperties().getProperty(DruidDataSourceFactory.PROP_PASSWORD);
            }

            if (encryptedPassword == null || encryptedPassword.length() == 0) {
                encryptedPassword = dataSource.getPassword();
            }
            // 获取公钥
            PublicKey publicKey = getPublicKey(dataSource.getConnectProperties(), info);
            // 调用ConfigTools#decrypt方法获得解密后的密文
            String passwordPlainText = ConfigTools.decrypt(publicKey, encryptedPassword);

            if (info != null) {
                info.setProperty(DruidDataSourceFactory.PROP_PASSWORD, passwordPlainText);
            } else {
                dataSource.setPassword(passwordPlainText);
            }
        } catch (Exception e) {
            thrownew IllegalArgumentException("Failed to decrypt.", e);
        }
    }

getPublicKey方法源码

publicstaticfinal String CONFIG_KEY;

static {
       CONFIG_KEY = "config.decrypt.key";
}
public static final String SYS_PROP_CONFIG_KEY = "druid.config.decrypt.key";
// 获取公钥
public PublicKey getPublicKey(Properties connectionProperties, Properties configFileProperties) {
        String key = null;
        if (configFileProperties != null) {
            key = configFileProperties.getProperty(CONFIG_KEY);
        }

        if (StringUtils.isEmpty(key) && connectionProperties != null) {
            key = connectionProperties.getProperty(CONFIG_KEY);
        }

        if (StringUtils.isEmpty(key)) {
            key = System.getProperty(SYS_PROP_CONFIG_KEY);
        }

        return ConfigTools.getPublicKey(key);
    }

首先会去从解析加密配制文件后的属性变量中获取公钥, 获取公钥的key为config.decrypt.key;若加密配制文件属性中不存在公钥,则去数据源的连接属性中获取key为config.decrypt.key对应的公钥,如果仍然没有则去系统属性变量中获取key为druid.config.decrypt.key对应的公钥。最后调用ConfigTools#getPublicKey方法根据传入的公钥返回一个PublicKey对象

3.5 DruidAbstractDataSource#setConnectionProperties方法源码

public void setConnectionProperties(String connectionProperties) {
        if (connectionProperties == null || connectionProperties.trim().length() == 0) {
            setConnectProperties(null);
            return;
        }
        // 多个连接属性使用分号分隔
        String[] entries = connectionProperties.split(";");
        Properties properties = new Properties();
        for (int i = 0; i < entries.length; i++) {
            String entry = entries[i];
            if (entry.length() > 0) {
                // 每个连接属性以=号分割成name和value两部分保存到properties属性中
                int index = entry.indexOf('=');
                if (index > 0) {
                    String name = entry.substring(0, index);
                    String value = entry.substring(index + 1);
                    properties.setProperty(name, value);
                } else {
                    // no value is empty string which is how java.util.Properties works
                    properties.setProperty(entry, "");
                }
            }
        }
        // 最后通过抽象方法调用实现类DruidDatasource类的setConnectProperties方法
        setConnectProperties(properties);
    }

其他的源码这里就不继续深入分析了,druid.jar包中涉及到ConfigTools,DruidDatasource和ConfigFilter三个类的源码掌握到这里对于实现数据库连接密码的加密和解密也已经足够了
————————————————

五 SpringBoot 2.X 同时整合 druid连接池加密 + dynamic-datasource 动态数据源

官方文档: dynamic-datasource-spring-boot-starter

1. 引入依赖

<dependency>
                <groupId>com.alibaba</groupId>
                <artifactId>druid-spring-boot-starter</artifactId>
                
    			<version>1.1.22</version>
         </dependency>
         <dependency>
                <groupId>com.baomidou</groupId>
                <artifactId>dynamic-datasource-spring-boot-starter</artifactId>
                <version>3.5.2</version>
          </dependency>
————————————————

2. 编写配置文件

# 数据源配置
spring:
  autoconfigure:
  	# 排除 Druid 自动配置
    exclude: com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceAutoConfigure
  datasource:
  	# 指定使用 Druid 数据源
    type: com.alibaba.druid.pool.DruidDataSource
    # 动态数据源文档 https://www.kancloud.cn/tracy5546/dynamic-datasource/content
    dynamic:
      #设置默认的数据源或者数据源组,默认值即为 master
      primary: master
      datasource:
        # 主库数据源
        master:
          driverClassName: com.mysql.cj.jdbc.Driver
          url: jdbc:mysql://192.168.0.121:3306/test1?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&autoReconnect=true
          username: root
          password: root
        # 从库数据源
        slave:
          driverClassName: com.mysql.cj.jdbc.Driver
          url: jdbc:mysql://192.168.0.122:3306/test2?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&autoReconnect=true
          username: root
          password: root
    druid:
      # 初始连接数
      initialSize: 5
      # 最小连接池数量
      minIdle: 10
      # 最大连接池数量
      maxActive: 20
      # 配置获取连接等待超时的时间
      maxWait: 60000
      # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
      timeBetweenEvictionRunsMillis: 60000
      # 配置一个连接在池中最小生存的时间,单位是毫秒
      minEvictableIdleTimeMillis: 300000
      # 配置一个连接在池中最大生存的时间,单位是毫秒
      maxEvictableIdleTimeMillis: 900000
      # 配置检测连接是否有效
      validationQuery: SELECT 1 FROM DUAL
————————————————

mysql启用密码插件 mysql启动密码_spring_05

3、测试

进入 Druid 控制台

mysql启用密码插件 mysql启动密码_后端_06

---------------------------------------------------------------------------------------------------------------------------------------------------------------

拓展: SpringBoot 对所有密码配置加密:Jasypt

背景

对于配置中的密码(DB, MQ, Redis等),甚至账号,在生产环境下存明文,不安全,不专业,不合适。

一把插着钥匙的锁,能说它是安全的吗?

操作流程
关于Jasypt实现对配置项的加密,网络上已经有很多这方面的资料,这里简要描述下步骤。

引入依赖

<dependency>
  <groupId>com.github.ulisesbocchio</groupId>
  <artifactId>jasypt-spring-boot-starter</artifactId>
  <version>1.18</version>
</dependency>

生成密文
如果计算机上有项目用过Jasypt的,那么在maven的仓库目录下会有Jasypt的jar包。如果本地仓库没有,先下载jar包。在jar包所在目录下打开cmd命令行,键入

java -cp jasypt-1.9.2.jar org.jasypt.intf.cli.JasyptPBEStringEncryptionCLI input="root" password=you-guess algorithm=PBEWithMD5AndDES

最下面输出的

qN66aPx0SrcFulrPfmMXOw==

是密文,在接下来要放入配置文件。
修改已有的配置
在已有的明文配置文件中,修改Jasypt密码相关配置。

spring:
  datasource:
    driverClassName: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/mp?serverTimezone=Asia/Shanghai&characterEncoding=UTF-8&useSSL=false
    username: root
    password: ENC(qN66aPx0SrcFulrPfmMXOw==)

# 配置日志
logging:
  level:
    root: info
    com.heartsuit.dao: trace
  pattern:
    console: '%p%m%n'

# 加密密钥
jasypt:
  encryptor:
    password: you-guess

上面的修改主要有:

mysql启用密码插件 mysql启动密码_spring_07

Note: 生成的密文要用ENC()包起来,这是Jasypt的要求。

测试修改后的配置 按照上述配置,应一切正常~~

Note: 需要注意的是:

用于生成加密后的密码的密钥不可放在配置文件或者代码中,加密密钥jasypt.encryptor.password=you-guess可以对密文解密。因此,上述配置若在团队内可见,没什么影响,但是如果配置文件万一被放到了公网上,相当于把钥匙插在锁上,白加密了。。在生产环境下,建议去掉加密密钥配置:

spring:
  datasource:
    driverClassName: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/mp?serverTimezone=Asia/Shanghai&characterEncoding=UTF-8&useSSL=false
    username: root
    password: ENC(qN66aPx0SrcFulrPfmMXOw==)

# 配置日志
logging:
  level:
    root: info
    com.heartsuit.dao: trace
  pattern:
    console: '%p%m%n'

仅去掉了:jasypt.encryptor.password=you-guess, 然后 jasypt.encryptor.password=you-guess可以换成另外两种方式传入:

  1. 通过服务启动参数
java -jar xxx.jar --jasypt.encryptor.password=you-guess
  1. 通过系统环境变量
    这个可以通过Idea IDE传入(开发环境),或者实际的系统环境变量传入(生产环境)。

Jasypt的加密与解密

通过Jasypt命令行的方式生产密文密码后,可以用Jasypt提供的API进行解密,当然,也可以用API的方式来完成加密。

加密与解密

@Component
public class StringEncryptDecrypt {

    @Autowired
    StringEncryptor encryptor;

    public String encrypt(String plainText) {
        // Encrypt
        String encrypted = encryptor.encrypt(plainText);
        System.out.println("Encrypted: " + encrypted);
        return encrypted;
    }

    public String decrypt(String encrypted) {
        // Decrypt
        String decrypted = encryptor.decrypt(encrypted);
        System.out.println("Decrypted: " + decrypted);
        return decrypted;
    }
}

总结

实现对配置文件敏感数据的加密,网上资源很多,但一定要注意安全性,不可以把公钥公开配置在文件中;还是开头那句话:

一把插着钥匙的锁,能说它是安全的吗?