Java: 加密配置文件中的敏感信息

前言

在项目中,我们一般是将数据库的连接信息(账号密码等)明文写在配置文件中,这样会有一个问题,我们很容易就把数据库的账号密码暴露出去了,因为yml文件在打成jar包后不会进行编译,还是一个yml文件,只要这个yml文件不小心泄露,我们数据库的连接信息就暴露了,下面我们来针对问题研究一下,看有没有什么加密的方式

先来看不对密码进行加密的配置

datasource:
  name: druidDataSource
  type: com.alibaba.druid.pool.DruidDataSource
  druid:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://ip:port/db?useUnicode=true&zeroDateTimeBehavior=convertToNull&autoReconnect=true&characterEncoding=utf-8&useSSL=false&serverTimezone=UTC
    username: root
    # 明文密码
    password: yourpasswd
    # 其它配置略................

这样直接配置的明文,显得不太安全


然后得知 Druid 有自带的有加密方式,通过自带的jar包可以生成密钥,我们来试一下

Druid加密方式

先去自己的Maven仓库中找到Druid的jar包,我这里是1.2.5版本,其它版本应该也可以,然后打开命令行(cmd或者bash)定位到jar包所在的路径,执行如下命令(Aa123456 为原始数据库密码)

java -cp druid-1.2.5.jar com.alibaba.druid.filter.config.ConfigTools Aa123456

生成了privateKey、publicKey、加密后的password

privateKey:MIIBVQIBADANBgkqhkiG9w0BAQEFAASCAT8wggE7AgEAAkEAtpgI1KFA4e/n/nLmUFgn70dEso/e3tq+wX49ek9pWFFGhogFmb7jDUyguSQ+Rvgt0txo7z2OdCRe9m4spzjGRwIDAQABAkBjfr65FlEjjDVvCi8DsrW4Ba6iWhEIgEuXZfGb9y+hBmTiEhWSW1UffdPD0ROTo9jg0suhNTxFcboPMF2GOdIBAiEA3gtH0jlMdi32fFCPkywh9oDzy6m5PAutrHpKEOBGm0cCIQDShFVt/eDraF2jiGzQooH+th8mdx/+OpHmDo7Q6Rv9AQIhANxlwA1n+IBZkQ7F/C0uIiwGxXcDaayzPtkzrS7hHtRjAiA+LaIB+9OcFFZb/+aL9QPKVMZ8mQDVGT2QosoiAEgpAQIhAIVq9yXbojHrW3G1cUsA1EkZ60pWjRgOrJrqbKXsULo4
publicKey:MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBALaYCNShQOHv5/5y5lBYJ+9HRLKP3t7avsF+PXpPaVhRRoaIBZm+4w1MoLkkPkb4LdLcaO89jnQkXvZuLKc4xkcCAwEAAQ==
password:QQ446Ed787nuj8CClXEQ+zWqLQJF+d7cQsGmRQSTF+++fXR5flrzQXGHPjewAS1/OHUpKUb+82MYD0Mj6+K2yg==

更新yml中数据的密码为加密数据

其中password为上一步中生成的加密password,key为上一步生成的publicKey

datasource:
  name: druidDataSource
  type: com.alibaba.druid.pool.DruidDataSource
  druid:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://ip:port/db?useUnicode=true&zeroDateTimeBehavior=convertToNull&autoReconnect=true&characterEncoding=utf-8&useSSL=false&serverTimezone=UTC
    username: root
    # 这里填上面获取到的password
    password: QQ446Ed787nuj8CClXEQ+zWqLQJF+d7cQsGmRQSTF+++fXR5flrzQXGHPjewAS1/OHUpKUb+82MYD0Mj6+K2yg==
    # 这里填上上面获取到的publicKey
    key: MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBALaYCNShQOHv5/5y5lBYJ+9HRLKP3t7avsF+PXpPaVhRRoaIBZm+4w1MoLkkPkb4LdLcaO89jnQkXvZuLKc4xkcCAwEAAQ==
    # config.decrypt=true 设定启动解密, config.decrypt.key=${spring.datasource.druid.key} 表示使用上一行配置的公钥来解密
    connection-properties: "druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000;config.decrypt=true;config.decrypt.key=${spring.datasource.druid.key}"
    initial-size: 1
    max-active: 20
    filters: stat,wall,slf4j,config # slf4j: 对应logback
    max-wait: 60000
    min-idle: 1
    test-while-idle: true
    test-on-borrow: false
    test-on-return: false
    pool-prepared-statements: true
    max-open-prepared-statements: 20
    async-init: true
    web-stat-filter:
      enabled: true
      exclusions: "*.js, *.gif, *.jpg, *.png, *.css, *.ico, /swagger*, /druid/*"
    stat-view-servlet:
      enabled: true
      login-username: druid
      login-password: druid
      allow: localhost

这样就实现了最简单的加密(也就比明文好一点),当然细心的朋友应该会发现,这也只是防君子不防小人,通过Druid自带的ConfigTools可以解密:

在Java代码中通过ConfigTools.decrypt(); 方法可以解析出密文

String key = "MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBALaYCNShQOHv5/5y5lBYJ+9HRLKP3t7avsF+PXpPaVhRRoaIBZm+4w1MoLkkPkb4LdLcaO89jnQkXvZuLKc4xkcCAwEAAQ==";
String encPwd = "QQ446Ed787nuj8CClXEQ+zWqLQJF+d7cQsGmRQSTF+++fXR5flrzQXGHPjewAS1/OHUpKUb+82MYD0Mj6+K2yg==";
System.out.println(ConfigTools.decrypt(key, encPwd)); // Aa123456

只要对 Druid加密规则清楚点的开发同学应该都可以轻松拿到密码,而且最开始wenshao大佬在实现加密功能的时候,并没有遵循RSA正常的流程,正常一般是通过公钥来加密数据,保管好私钥,然后用私钥来解密数据

细心的同学应该发现了,上面填的key是publicKey,就把是把publicKey当成了privateKey,好像搞反了,当然,也有人在github上问过wenshao大佬这个问题,wenshao也针对这个问题进行了 回复,大家可以去看一下

当然,上面只是用Druid默认的生成方式,其实还可以用自定义生成RSA公钥和私钥的方式来加密,并把自己的加密提供给Druid进行回调,这里我不详细讲述,感兴趣的朋友可以参考下 这篇文章

其实还有一种加密方式,我们来看一下

Jasypt加密方式

jasypt 也能实现相同的功能,不同于Druid默只对密码进行加密,jasypt 就灵活得多,只要我们定义好加密规则,我们可以对数据库链接、账号、密码都加上密,大家可以查下官方文档是怎么用的,我这里把我的实现贴出来给大家参考一下

首先引入依赖

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

新建一个解析类

package com.yinchd.web.config;

import org.jasypt.encryption.StringEncryptor;
import org.jasypt.encryption.pbe.PooledPBEStringEncryptor;
import org.jasypt.encryption.pbe.StandardPBEByteEncryptor;
import org.jasypt.encryption.pbe.config.SimpleStringPBEConfig;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * Jasypt 配置
 */
@Configuration
public class JasyptConfig {

    @Bean("jasyptStringEncryptor")
    public StringEncryptor stringEncryptor() {
        PooledPBEStringEncryptor encryptor = new PooledPBEStringEncryptor();
        SimpleStringPBEConfig config = new SimpleStringPBEConfig();
        // 加密盐值
        config.setPassword("Ayinchd@*(j^z");
        // 加密算法
        config.setAlgorithm(StandardPBEByteEncryptor.DEFAULT_ALGORITHM);
        // key迭代次数
        config.setKeyObtentionIterations("1000");
        // 池大小
        config.setPoolSize("1");
        config.setProviderName("SunJCE");
        // 随机盐生成器
        config.setSaltGeneratorClassName("org.jasypt.salt.RandomSaltGenerator");
        config.setIvGeneratorClassName("org.jasypt.iv.RandomIvGenerator");
        // 加密后输出字符串编码方式
        config.setStringOutputType("base64");
        encryptor.setConfig(config);
        return encryptor;
    }

    public static void main(String[] args) {
        StringEncryptor encryptor = new JasyptConfig().stringEncryptor();
        String url = "jdbc:mysql://ip:port/db?useUnicode=true&zeroDateTimeBehavior=convertToNull&autoReconnect=true&characterEncoding=utf-8&useSSL=false&serverTimezone=UTC";
        String user = "root";
        String pwd = "Aa123456";
        System.out.println(encryptor.encrypt(url));
        System.out.println(encryptor.encrypt(user));
        System.out.println(encryptor.encrypt(pwd));
        // Zg2cMlfZ4fw58+gIuJsl1Tf9O1mPtubldcbXX/PW5WCDDMk2zY73hYc+k2/XgnfgFAsAqdaySVSM1zVKw6DbjhfZnQa/bBtB/mirMgPXJWHx6sLlfrOBHyh07hwfbC5+eI1kx3Sif19VSAe20ZEHu+MB3H1orJ8qBwe2M/BfDbVz4mGmfJ6cLZvfYBemLvOYvV9fgGrsMTtF3VkyNHU4BXnzy8PWMzhg
        // 7DIiWfG69scrYvG12ql5o6bKESGzXXxv
        // OlNNAnO5JZ+Bdd2Rcz44JnOMgJCtdc+xZyvQH9+XRFI=
    }

}

更新 jasypt 加密的数据到配置文件

datasource:
  name: druidDataSource
  type: com.alibaba.druid.pool.DruidDataSource
  druid:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: ENC(Zg2cMlfZ4fw58+gIuJsl1Tf9O1mPtubldcbXX/PW5WCDDMk2zY73hYc+k2/XgnfgFAsAqdaySVSM1zVKw6
    username: ENC(7DIiWfG69scrYvG12ql5o6bKESGzXXxv)
    password: ENC(OlNNAnO5JZ+Bdd2Rcz44JnOMgJCtdc+xZyvQH9+XRFI=)
    connection-properties: "druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000;"
    initial-size: 1
    max-active: 20
    filters: stat,wall,slf4j,config # slf4j: 对应logback
    # 其它配置略................

因为 JasyptConfig 中已将配置(包含password)注入到容器中,所以完成上述配置后就已经搞定了,系统已经可以正常解析到配置文件中ENC(***) 加密的数据

同理,我们也可以据此来解密:

StringEncryptor encryptor = new JasyptConfig().stringEncryptor();
String url = "Zg2cMlfZ4fw58+gIuJsl1Tf9O1mPtubldcbXX/PW5WCDDMk2zY73hYc+k2/XgnfgFAsAqdaySVSM1zVKw6DbjhfZnQa/bBtB/mirMgPXJWHx6sLlfrOBHyh07hwfbC5+eI1kx3Sif19VSAe20ZEHu+MB3H1orJ8qBwe2M/BfDbVz4mGmfJ6cLZvfYBemLvOYvV9fgGrsMTtF3VkyNHU4BXnzy8PWMzhg";
String user = "7DIiWfG69scrYvG12ql5o6bKESGzXXxv";
String pwd = "OlNNAnO5JZ+Bdd2Rcz44JnOMgJCtdc+xZyvQH9+XRFI=";
System.out.println(encryptor.decrypt(url));
System.out.println(encryptor.decrypt(user));
System.out.println(encryptor.decrypt(pwd));
// jdbc:mysql://ip:port/db?useUnicode=true&zeroDateTimeBehavior=convertToNull&autoReconnect=true&characterEncoding=utf-8&useSSL=false&serverTimezone=UTC
// root
// Aa123456

总结

总体来说,上述两种方式都不能称之为一种安全的方式

因为不管是druid的配置方式中,我们使用Druid默认的加密也好,使用自定义的RSA加解密方式也好,我们始终要将我们的加密key给暴露在文件或者代码中

Jasypt 也一样,我们要将我们的password写在了JasyptConfig这个类中,跟配置文件相比,可能硬编码编译成class文件后会稍微安全一丢丢,但是通过反编译手段,我们一样可以拿到密钥信息

只能说这些手段都是防君子不防小人,能让配置文件上的信息丢失后,能给破解者制造一点点难度。


如果大家其它更安全的加密方式,欢迎在下方评论区留言!


版权声明:本文为sageyin原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。