前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >巧用Druid数据源实现数据库连接密码的加密解密

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

作者头像
用户3587585
发布2022-04-14 15:08:41
4.5K0
发布2022-04-14 15:08:41
举报
文章被收录于专栏:阿福谈Web编程阿福谈Web编程

前言

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

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

最近在工作中就接触到了这样一个需求,要求把数据库密码以密文的形式保存在配置文件中,解密密钥保存在不同的配置文件中,在初始化Datasource bean的时候再将拿到的加密密码进行解密。

经过一番调研后,笔者发现阿里强大的druid数据源就能很好的实现这个需求。本文不仅带领读者实现这个数据库连接密码的加密解密功能,还带领读者把其中的流程和原理彻底搞清楚。

1 Druid连接池介绍

1.1 什么是Druid连接池

Druid连接池是阿里巴巴开源的数据库连接池项目。Druid连接池为监控而生,内置强大的监控功能,监控特性不影响性能。功能强大,能防SQL注入,内置Loging能诊断Hack应用行为。

1.2 竞品对比

图片表格来源:github网站Druid连接池介绍

从上表可以看出,Druid连接池在性能、监控、诊断、安全、扩展性这些方面远远超出同类竞品。

1.3 内置ExceptionSorter

Druid连接池内置经过长期反馈验证过的ExceptionSorter 。

ExceptionSorter 的作用是:在数据库服务器重启、网络抖动、连接被服务器关闭等异常情况下,连接发生了不可恢复异常,将连接从连接池中移除,保证连接池在异常发生时情况下正常工作。ExceptionSorter是连接池稳定的关键特性,没有ExceptionSorter 的连接池,不能认为是有稳定性保障的连接池

1.4 为监控而生

Druid连接池最初就是为监控系统采集jdbc运行信息而生的,它内置了StatFilter 功能,能采集非常完备的连接池执行信息Druid连接池内置了能和Spring/Servlet关联监控的实现,使得监控Web应用特别方便。Druid连接池内置了一个监控页面,提供了非常完备的监控信息,可以快速诊断系统的瓶颈。

Druid连接池的监控信息主要是通过StatFilter 采集的,采集的信息非常全面,包括SQL执行、并发、慢查询、执行时间区间分布等。具体配置可以看这里:https://github.com/alibaba/druid/wiki/配置_StatFilter

1.5 诊断支持

Druid连接池内置了LogFilter,将Connection/Statement/ResultSet相关操作的日志输出,可以用于诊断系统问题,也可以用于Hack一个不熟悉的系统。

LogFilter可以输出连接申请/释放,事务提交回滚,Statement的Create/Prepare/Execute/Close,ResultSet的Open/Next/Close,通过LogFilter可以详细诊断一个系统的Jdbc行为。

LogFilter有Log4j、Log4j2、Slf4j、CommsLog等实现,具体配置看这里: https://github.com/alibaba/druid/wiki/配置_LogFilter

1.6 防SQL注入

SQL注入攻击是黑客对数据库进行攻击的常用手段,Druid连接池内置了WallFilter 提供防SQL注入功能,在不影响性能的同时防御SQL注入攻击。

Druid连接池内置了一个功能完备的SQL Parser,能够完整解析mysql、sql server、oracle、postgresql的语法,通过语意分析能够精确识别SQL注入攻击。

上面我们介绍了druid数据源的众多功能,不过本文只专注于解锁其中的Filter扩展中的ConfigFilter实现数据库连接密码的加密解密功能,目的在于防止因代码中出现数据库连接明文密码而导致连接密码泄露,有效提升系统数据库访问的安全性能。

2 如何生成公私钥

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

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

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

代码语言:javascript
复制
<dependency>
	<groupId>com.alibaba</groupId>
	<artifactId>druid</artifactId>
	<version>1.2.8</version>
</dependency>

ConfigTools#main方法源码

代码语言:javascript
复制
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));
    }

2.1 执行java命令生成公私钥

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

代码语言:javascript
复制
java -cp druid-1.2.8.jar com.alibaba.druid.filter.config.ConfigTools admin123

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

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

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

ConfigTools#genKeyPair方法源码

代码语言:javascript
复制
public static 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;
    }

public static byte[][] genKeyPairBytes(int keySize) throws NoSuchAlgorithmException, NoSuchProviderException {
        byte[][] keyPairBytes = new byte[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工具类写的测试类如下:

代码语言:javascript
复制
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方法后也可以看到控制台中打印出了一对公私钥和加密和解密后的密码

代码语言:javascript
复制
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

3 Druid数据源配置ConfigFilter

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

ConfigFilter的作用包括:

  • 从配置文件中读取配置
  • 从远程http文件中读取配置
  • 为数据库密码提供解密功能

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

代码语言:javascript
复制
@Configuration
public class 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) {
            throw new BeanCreationException("create DruidDataSource bean failed, caused by " + e.getMessage());
        }
        
    }
    
}

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

代码语言:javascript
复制
@Configuration
public class 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) {
            throw new BeanCreationException("create DruidDataSource bean failed, caused by " + e.getMessage());
        }
        
    }
    
}

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

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

代码语言:javascript
复制
java -jar application-<版本号>-SNAPSHOT.jar -Ddruid.filters=config 

4 ConfigFilter解密原理分析

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

4.1 DruidDataSource#setFilters方法

代码语言:javascript
复制
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方法加载过滤器

4.2 FilterManager类静态代码块

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

代码语言:javascript
复制
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方法加载过滤器配置

代码语言:javascript
复制
 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文件中有哪些过滤器

代码语言:javascript
复制
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属性中。

4.3 FilterManager#loadFilter方法

代码语言:javascript
复制
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) {
                    throw new SQLException("load managed jdbc driver event listener error. " + filterName, e);
                } catch (IllegalAccessException e) {
                    throw new SQLException("load managed jdbc driver event listener error. " + filterName, e);
                } catch (RuntimeException e) {
                    throw new 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) {
            throw new SQLException("load managed jdbc driver event listener error. " + filterName, e);
        }
    }

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

4.4 数据库连接密文解密的具体实现

ConfigFilter类中有个init方法,正是在这个初始化方法中完成了数据源加密密码的解密

代码语言:javascript
复制
 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) {
            throw new IllegalArgumentException("Config DataSource error.", e);
        }
    }

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

DruidDatasource#init方法中触发ConfigFilter#init方法的源码

代码语言:javascript
复制
for (Filter filter : filters) {
       filter.init(this);
}

loadPropertyFromConfigFile方法源码

代码语言:javascript
复制
public static 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) {
                throw new IllegalArgumentException("Cannot load remote config file from the [config.file=" + configFile
                                                   + "].");
            }

            return info;
        }

        return null;
    }

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

isDecrypt方法源码

代码语言:javascript
复制
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方法源码分析

代码语言:javascript
复制
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) {
            throw new IllegalArgumentException("Failed to decrypt.", e);
        }
    }

getPublicKey方法源码

代码语言:javascript
复制
public static final 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对象

4.5 DruidAbstractDataSource#setConnectionProperties方法源码

代码语言:javascript
复制
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包中涉及到ConfigToolsDruidDatasourceConfigFilter三个类的源码掌握到这里对于实现数据库连接密码的加密和解密也已经足够了。

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

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

5.1 修改application-dev.properties文件

删除之前的spring.datasource前缀配制的数据源信息,添加以druid为前缀的数据源配制信息

修改后的dev环境配制文件如下,其他环境上线前其对应的环境属性文件也要作出相应的修改

代码语言:javascript
复制
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

5.2 新建druid.configFile.properties文件

在项目的resources目录下新建用于配置加密属性的配置文件druid.configFile.properties文件文件中的内容如下:

注意:公钥和密文一定要分开保存以有效降低数据库连接密码泄露的风险,公钥泄露一样有泄密的风险

代码语言:javascript
复制
#解密标识
config.decrypt=true
#解密公钥
config.decrypt.key=MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAIIax7OcAFG+0HHhMH27w+mjIzTVeWKE8id85GmnZyCD+P52eX+o3YlyuAW/g63vDTX/ZFkuhmSSmZzpeVnjfnMCAwEAAQ==

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

代码语言:javascript
复制
@Configuration
@ConfigurationProperties(prefix = "druid")
public class DruidDatasourceProperties {

    private String url;

    private String username;

    private String password;

    private boolean testWhileIdle;

    private String validationQuery;

    private String filters;

    private long timeBetweenEvictionRunsMillis;

    private long maxWait;

    private boolean failFast;

    private long phyTimeoutMillis;

    private long minEvictableIdleTimeMillis;

    private long maxEvictableIdleTimeMillis;

    private boolean keepAlive;

    private boolean useUnfairLock;

    private String driverClassName;

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

5.4 新建Druid数据源配置类

代码语言:javascript
复制
@Configuration
public class DataSourceConfig {

    private static final 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);
            throw new BeanCreationException("create DruidDataSource bean failed, caused by " + e.getMessage());
        }
        return dataSource;
    }

}

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

5.5 测试效果

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

代码语言:javascript
复制
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数据源的其他扩展功能,并撰文发表到自己的公众号上。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2022-04-04,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 阿福谈Web编程 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言
  • 1 Druid连接池介绍
    • 1.1 什么是Druid连接池
      • 1.2 竞品对比
        • 1.3 内置ExceptionSorter
          • 1.4 为监控而生
            • 1.5 诊断支持
              • 1.6 防SQL注入
              • 2 如何生成公私钥
                • 2.1 执行java命令生成公私钥
                  • 2.2 利用ConfigTools工具类生成公私钥
                  • 3 Druid数据源配置ConfigFilter
                    • 3.1 配置文件从本地文件读取
                      • 3.2 配置文件从远程http服务器中读取
                        • 3.3 通过jvm启动参数来使用ConfigFilter
                        • 4 ConfigFilter解密原理分析
                          • 4.1 DruidDataSource#setFilters方法
                            • 4.2 FilterManager类静态代码块
                              • 4.3 FilterManager#loadFilter方法
                                • 4.4 数据库连接密文解密的具体实现
                                  • 4.5 DruidAbstractDataSource#setConnectionProperties方法源码
                                  • 5 项目中实现数据库连接加密密码解密实战
                                    • 5.1 修改application-dev.properties文件
                                      • 5.2 新建druid.configFile.properties文件
                                        • 5.3 新建druid数据源属性配置类
                                          • 5.4 新建Druid数据源配置类
                                            • 5.5 测试效果
                                            相关产品与服务
                                            数据库
                                            云数据库为企业提供了完善的关系型数据库、非关系型数据库、分析型数据库和数据库生态工具。您可以通过产品选择和组合搭建,轻松实现高可靠、高可用性、高性能等数据库需求。云数据库服务也可大幅减少您的运维工作量,更专注于业务发展,让企业一站式享受数据上云及分布式架构的技术红利!
                                            领券
                                            问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档