明天的明天 永远的永远 未知的一切 我与你一起承担 ??

是非成败转头空 青山依旧在 几度夕阳红 。。。
  博客园  :: 首页  :: 管理

springboot之druid数据库密码加密实战

Posted on 2024-04-28 09:22  且行且思  阅读(2798)  评论(1编辑  收藏  举报

前言

最近接了一个外包单(基于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数据库密码加密的讲解,可能是因为这个太简单了原因吧