巧用Druid数据源实现数据库连接密码的加密解密功能

前言

一个系统的数据库的连接密码作为一个非常重要的安全数据,其安全非常重要。而在代码的配置文件中直接存放明文密码提交到代码仓库后显然有泄露的风险。

一旦数据库连接密码泄露,那么黑客就能直接访问数据库并篡改数据。对于一个拥有重要客户信息数据的大型公司而言,其应用系统出现数据库连接密码泄露将是一件非常严重的灾难事件,不仅很可能损失重要客户,还可能面临被客户索赔的风险,从而给公司造成重大经济损失。因此程序员在给公司设计和开发应用系统时必须考虑到数据库访问的安全问题。

最近在工作中就接触到了这样一个需求,要求把数据库密码以密文的形式保存在配置文件中,解密密钥保存在不同的配置文件中,在初始化Datasource bean的时候再将拿到的加密密码进行解密。
经过一番调研后,笔者发现阿里强大的druid数据源就能很好的实现这个需求。本文不仅带领读者实现这个数据库连接密码的加密解密功能,并带领读者把其中的流程和原理彻底搞清楚。

1 如何生成公私钥

在非对称加密算法领域,密钥都是成对出现的,私钥用来解密密码生成密文,公钥用来解密密文。

在阿里的druid.jar包中存在一个工具类ConfigTools可用来生成一个对公私钥,我们参考该工具类中的main方法生成公私钥的方法来写一个生成公私钥的测试类

首先我们需要在Maven项目的引入druid的jar包的maven依赖,笔者用的是1.2.8版本

<dependency>
	<groupId>com.alibaba</groupId>
	<artifactId>druid</artifactId>
	<version>1.2.8</version>
</dependency>

ConfigTools#main方法源码

public static void main(String[] args) throws Exception {
    
    
        String password = args[0];
        // 密钥位数为512位
        String[] arr = genKeyPair(512);
        System.out.println("privateKey:" + arr[0]);
        System.out.println("publicKey:" + arr[1]);
        System.out.println("password:" + encrypt(arr[0], password));
    }

1.1 执行java命令生成公私钥

在本地maven仓库的druid-1.2.8.jar所在目录的控制台中执行如下命令生成公私钥和加密后的密文。其中admin123为密码明文, 读者可根据自己的实际需要改成自己的数据库连接密码明文,执行上面的命令回车后会在控制台中打印出一对公私钥和加密后的密文。

java -cp druid-1.2.8.jar com.alibaba.druid.filter.config.ConfigTools admin123

keyPair
然后可以将控制台中的publicKey对应的内容和加密密文拷贝到项目中的配置文件中

1.2 利用ConfigTools工具类生成公私钥

ConfigTools类中生成公私钥的方法是一个public修改的静态方法,我们可以通过ConfigTools类直接调用
ConfigTools#genKeyPair方法源码

publicstatic String[] genKeyPair(int keySize) throws NoSuchAlgorithmException, NoSuchProviderException {
    
    
        byte[][] keyPairBytes = genKeyPairBytes(keySize);
        String[] keyPairs = new String[]{
    
    Base64.byteArrayToBase64(keyPairBytes[0]), Base64.byteArrayToBase64(keyPairBytes[1])};
        return keyPairs;
    }

publicstaticbyte[][] genKeyPairBytes(int keySize) throws NoSuchAlgorithmException, NoSuchProviderException {
    
    
        byte[][] keyPairBytes = newbyte[2][];
        KeyPairGenerator gen = KeyPairGenerator.getInstance("RSA", "SunRsaSign");
        gen.initialize(keySize, new SecureRandom());
        KeyPair pair = gen.generateKeyPair();
        keyPairBytes[0] = pair.getPrivate().getEncoded();
        keyPairBytes[1] = pair.getPublic().getEncoded();
        return keyPairBytes;
    }

通过查看genKeyPairBytes方法中的源码,我们可以看到ConfigTools生成公私钥时使用了RSA算法
我们使用ConfigTools工具类写的测试类如下:

public class GenPublicAndPrivateKeyTest {
    
    

    public static void main(String[] args) {
    
    
        String password = "admin1234";
        try {
    
    
            // 利用阿里的ConfigTools工具类来生成一对公私钥,私钥用来加密,公钥用来解密
            String[] keyParis = ConfigTools.genKeyPair(512);
            String privateKey = keyParis[0];
            String publicKey = keyParis[1];
            System.out.println("privateKey="+privateKey);
            System.out.println("publicKey="+publicKey);
            String encryptPassword = ConfigTools.encrypt(privateKey, password);
            System.out.println("encryptPassword="+encryptPassword);
            String decryptPassword = ConfigTools.decrypt(publicKey, encryptPassword);
            System.out.println("decryptPassword="+decryptPassword);
        } catch (NoSuchAlgorithmException e) {
    
    
            e.printStackTrace();
        } catch (NoSuchProviderException e) {
    
    
            e.printStackTrace();
        } catch (Exception e) {
    
    
            e.printStackTrace();
        }

    }
}

在IDEA中执行测试类中的main方法后也可以看到控制台中打印出了一对公私钥和加密和解密后的密码

privateKey=MIIBVQIBADANBgkqhkiG9w0BAQEFAASCAT8wggE7AgEAAkEAkmQdOMn5kE/csViZObTdKCQjWRud4vQbQwb5j5JwTYHgPXzMnpitgcFdehthT1uNV6eU70dt1+L0Xxz86MccbwIDAQABAkBhZrU+tLwU9d4cLZv9lkZTz/+o6UQa3lpJNZnUmhWYq2CGkucjQ5ezk3TwDRHhl5UHsJYKag0B0A0EkbCLXQxZAiEAyeGX2IvKKKor1gtHdfbOqPjjeu9U0n4uIz//a5vlNw0CIQC5omy2prmr7fOE10PUnyaim9lWkFjzlzQZQ+X4OPcCawIhAJ9LYoV7yAhOPkimnbx3AppRyS03q7Zr2fv2g5RlbngBAiEAuBloXYBd1S/IeW8PezdXFp8fXSUMwo+rAH+A+7pq5f8CIFP/dk7hZrzF2nNRzeHRHfFUKBZFeQM48mpO8PCpEstl
publicKey=MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAJJkHTjJ+ZBP3LFYmTm03SgkI1kbneL0G0MG+Y+ScE2B4D18zJ6YrYHBXXobYU9bjVenlO9Hbdfi9F8c/OjHHG8CAwEAAQ==
encryptPassword=QRcR+m5mq1k7sJJuBuKs+vhDtlCz6AqqMblQw6pkDe7s56x13Dhr9b/znoMeQqFcwiufS4gl92pNtIA0EMchPg==
decryptPassword=admin1234

2 Druid数据源配置ConfigFilter

数据库密码直接写在配置中,对运维安全来说,是一个很大的挑战。Druid为此提供一种数据库密码加密的手段ConfigFilter。
ConfigFilter的作用包括:

  • 从配置文件中读取配置

  • 从远程http文件中读取配置

  • 为数据库密码提供解密功能

2.1 配置文件从本地文件读取

@Configuration
publicclass DruidDatasourceConfig {
    
    
    
    @Bean(initMethod = "init", destroyMethod = "close")
    public DruidDatasource datasource(){
    
    
        DruidDataSource dataSource = new DruidDataSource();
        try {
    
    
            dataSource.setFilters("config");
            dataSource.setConnectionProperties("config.file=file:///home/admin/druid-pool.properties")
        } catch(SQLException e) {
    
    
            thrownew BeanCreationException("create DruidDataSource bean failed, caused by " + e.getMessage());
        }
        
    }
    
}

2.2 配置文件从远程http服务器中读取

@Configuration
publicclass DruidDatasourceConfig {
    
    
    
    @Bean(initMethod = "init", destroyMethod = "close")
    public DruidDatasource datasource(){
    
    
        DruidDataSource dataSource = new DruidDataSource();
        try {
    
    
            dataSource.setFilters("config");
            dataSource.setConnectionProperties("config.file=http://127.0.0.1/druid-pool.properties")
        } catch(SQLException e) {
    
    
            thrownew BeanCreationException("create DruidDataSource bean failed, caused by " + e.getMessage());
        }
        
    }
    
}

2.3 通过jvm启动参数来使用ConfigFilter

DruidDataSource支持jvm启动参数配置filters,所以你可以在执行应用jar包的命令时添加配置filters的jvm启动参数

java -jar application-<版本号>-SNAPSHOT.jar -Ddruid.filters=config 

3 ConfigFilter解密原理分析

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

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类实例添加到DruidDataSourceList数据结构的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);
        }
    }

上面这个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三个类的源码掌握到这里对于实现数据库连接密码的加密和解密也已经足够了

4 项目中实现数据库连接加密密码解密实战

为避免重复搭建项目,这一部分内容仍然以本人在上一篇文章手把手带你在集成SpringSecurity的SpringBoot应用中添加短信验证码登录认证功能中使用的项目blogserver项目为基础

4.1 修改application-dev.properties文件

删除之前的spring.datasource前缀配制的数据源信息,添加以druid为前缀的数据源配制信息
修改后的dev环境配制文件如下,其他环境上线前其对应的环境属性文件也要作出相应的修改

crossOrigin=http://localhost:3000
knife4j.production=false

# redis配置
spring.redis.client-name=redis-client
spring.redis.host=127.0.0.1
spring.redis.port=6379
spring.redis.database=0
spring.redis.jedis.pool.max-active=8
spring.redis.jedis.pool.max-wait=5000ms
spring.redis.jedis.pool.min-idle=1
spring.redis.jedis.pool.time-between-eviction-runs=30000ms

#DruidDatasourcePropperties
druid.url=jdbc:mysql://localhost:3306/vueblog2?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
druid.username=vueblog
#数据库连接密码使用私钥加密后的密文
druid.password=PBxm3J2QKCy+60YG64WNn4rHh5Fvskq1fbRd4tj/d+dtcT/HJRkvTB4CAIQLMThLtqiPch0iegrsdHT5X9e/mg==
druid.testWhileIdle=true
druid.validationQuery=select 1 from dual
druid.filters=config
druid.timeBetweenEvictionRunsMillis=60000
druid.maxWait=30000
druid.failFast=true
druid.phyTimeoutMillis=30000
druid.minEvictableIdleTimeMillis=30000
druid.maxEvictableIdleTimeMillis=60000
druid.keepAlive=true
druid.useUnfairLock=true
druid.driverClassName=com.mysql.cj.jdbc.Driver
#连接属性
druid.connectProperties=config.file=druid.configFile.properties

druid数据源中的配置属性释义可参考DruidDataSource配置属性列表https://github.com/alibaba/druid/wiki/DruidDataSource%E9%85%8D%E7%BD%AE%E5%B1%9E%E6%80%A7%E5%88%97%E8%A1%A8

4.2 新建druid.configFile.properties文件

在项目的resources目录下新建用于配置加密属性的配置文件druid.configFile.properties文件文件中的内容如下:
注意:公钥和密文一定要分开保存以有效降低数据库连接密码泄露的风险,公钥泄露一样有泄密的风险

#解密标识
config.decrypt=true
#解密公钥
config.decrypt.key=MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAIIax7OcAFG+0HHhMH27w+mjIzTVeWKE8id85GmnZyCD+P52eX+o3YlyuAW/g63vDTX/ZFkuhmSSmZzpeVnjfnMCAwEAAQ==

4.3 新建druid数据源属性配置类

@Configuration
@ConfigurationProperties(prefix = "druid")
public class DruidDatasourceProperties {
    
    

    private String url;

    private String username;

    private String password;

    privateboolean testWhileIdle;

    private String validationQuery;

    private String filters;

    privatelong timeBetweenEvictionRunsMillis;

    privatelong maxWait;

    privateboolean failFast;

    privatelong phyTimeoutMillis;

    privatelong minEvictableIdleTimeMillis;

    privatelong maxEvictableIdleTimeMillis;

    privateboolean keepAlive;

    privateboolean useUnfairLock;

    private String driverClassName;

    private String connectProperties;
    // ......省略属性的set和get方法
}

4.4 新建Druid数据源配置类

@Configuration
public class DataSourceConfig {
    
    

    privatestaticfinal Logger logger = LoggerFactory.getLogger(DataSourceConfig.class);

    @Resource
    private DruidDatasourceProperties datasourceProperties;

    @Bean(initMethod = "init", destroyMethod = "close")
    public DruidDataSource dataSource() {
    
    
        DruidDataSource dataSource = new DruidDataSource();
        dataSource.setUseUnfairLock(datasourceProperties.isUseUnfairLock());
        dataSource.setUrl(datasourceProperties.getUrl());
        dataSource.setUsername(datasourceProperties.getUsername());
        dataSource.setPassword(datasourceProperties.getPassword());
        dataSource.setConnectionProperties(datasourceProperties.getConnectProperties());
        dataSource.setKeepAlive(datasourceProperties.isKeepAlive());
        dataSource.setDriverClassName(datasourceProperties.getDriverClassName());
        dataSource.setFailFast(datasourceProperties.isFailFast());
        dataSource.setValidationQuery(datasourceProperties.getValidationQuery());
        dataSource.setTestWhileIdle(datasourceProperties.isTestWhileIdle());
        dataSource.setTimeBetweenEvictionRunsMillis(datasourceProperties.getTimeBetweenEvictionRunsMillis());
        dataSource.setMaxWait(datasourceProperties.getMaxWait());
        dataSource.setPhyTimeoutMillis(datasourceProperties.getPhyTimeoutMillis());
        dataSource.setMinEvictableIdleTimeMillis(datasourceProperties.getMinEvictableIdleTimeMillis());
        dataSource.setMaxEvictableIdleTimeMillis(datasourceProperties.getMaxEvictableIdleTimeMillis());
        dataSource.setInitExceptionThrow(true);
        try {
    
    
            dataSource.setFilters(datasourceProperties.getFilters());
        } catch (SQLException e) {
    
    
            logger.error("create DruidDataSource bean failed", e);
            thrownew BeanCreationException("create DruidDataSource bean failed, caused by " + e.getMessage());
        }
        return dataSource;
    }

}

通过上面的DataSourceConfig类我们完成将DruidDataSource实例化并设置connectionProperties属性和filters属性, 并将完成支持解密的Druid数据源实例以bean的形式注册到Spring IOC容器中。

4.5 测试效果

然后,我们在IDEA中启动blogserver应用,在控制台中出现如下两行日志说明Druid数据源完成了初始化,并且使用了ConfigFilter

2022-04-03 16:33:51.791  INFO 13092 --- [           main] c.a.druid.filter.config.ConfigFilter     : DruidDataSource Config File load from : druid.configFile.properties
2022-04-03 16:33:53.427  INFO 13092 --- [           main] com.alibaba.druid.pool.DruidDataSource   : {
    
    dataSource-1} inited

项目启动成功后,我们调用登录接口正常返回数据,则证明我们在项目中使用Druid数据源完成了数据库连接密码加密解密功能的修改。
由此可见阿里的druid数据源是一款功能非常强大且丰富的产品,Filter扩展中还有其他强大的功能,如用于监控的StartFilter、用于诊断的LogFilter以及用于防SQL注入的WallFilter需要我们自己去解锁。

关于如何使用druid数据源的这些强大的Filter扩展功能,大家可通过下面的参考文章中的链接至github上druid的wiki文章学习。后面有时间的话,笔者也会尝试在项目中使用druid数据源的其他扩展功能,并撰文发表到自己的公众号上。

6 写在最后

本文首发个人微信公众号【阿福谈Web编程】,欢迎各位认可我的读者朋友加个公号关注交流,让我们在技术精进的路上前行不孤单!

猜你喜欢

转载自blog.csdn.net/heshengfu1211/article/details/123951560