1. 项目概述:为什么我们需要对配置文件加密?
在Spring Boot项目里,数据库连接信息、第三方服务的密钥、Redis密码这些敏感配置,通常都明晃晃地写在application.yml或application.properties里。开发时为了方便,直接写明文密码root:123456是常态。但项目一旦要部署到生产环境,或者代码要提交到Git仓库,问题就来了。把包含明文密码的配置文件推送到Git,无异于把自家大门的钥匙挂在公告栏上;而生产服务器的配置文件如果被轻易获取,数据安全防线将形同虚设。
我经历过几次安全审计,扫描工具一跑,直接就把配置文件里的明文密码列为高危漏洞。所以,给配置文件加密,尤其是数据库密码这类核心敏感信息加密,从一个“可选项”变成了一个“必选项”。MyBatis-Plus作为我们操作数据库的利器,其数据源配置恰恰是加密的重灾区。手动解密再注入虽然可行,但繁琐且容易出错。今天要聊的,就是如何将MyBatis-Plus与配置加密方案无缝整合,实现配置项“落地即密文,运行时解密”,让安全性和开发便利性兼得。
这个方案的核心价值在于无侵入性和自动化。我们不需要修改MyBatis-Plus的核心代码,也不需要重写SqlSessionFactory,而是利用Spring Boot的配置加载机制,在配置被解析、数据源被创建之前,完成解密操作。这样,MyBatis-Plus感知到的永远是正确的、已解密的连接信息,整个加解密过程对它来说是透明的。
2. 加密方案选型与设计思路拆解
面对“加密”这个需求,第一步不是埋头写代码,而是选对方案。不同的场景,适合的加密方式和集成策略完全不同。
2.1 主流配置加密方案对比
目前,在Spring Boot生态中,主要有以下几种思路:
- Jasypt (Java Simplified Encryption):老牌选手,社区成熟,支持多种加密算法(如PBEWithMD5AndDES),与Spring Boot集成有现成的
starter。其原理是通过特定的格式(如ENC(密文))在配置文件中标识需要解密的内容,并提供一个StringEncryptorBean在运行时解密。 - Spring Cloud Config Server的加密功能:如果你在用Spring Cloud Config做分布式配置中心,它原生支持对称/非对称加密。但这属于“重量级”方案,引入了额外的服务组件,适合微服务架构,对于单体的、只想加密本地配置的项目来说过于复杂。
- 自定义
EnvironmentPostProcessor:这是最灵活、侵入性最低的方案。通过实现EnvironmentPostProcessor接口,我们可以在Spring Boot应用上下文准备环境、但还未创建Bean之前,拦截并处理所有的配置属性。你可以在这里注入自己的解密逻辑。 - 使用Vault或阿里云KMS等外部密钥管理服务:安全性最高,将密钥与代码、配置完全分离。但同样会引入外部依赖和网络调用,增加架构复杂度和运维成本。
对于大多数结合MyBatis-Plus的项目,我的建议是:优先考虑Jasypt或自定义EnvironmentPostProcessor。如果项目已经用了Jasypt,继续用它是最平滑的。如果是新项目,或者对Jasypt的默认算法有顾虑,想更精细地控制解密过程,那么自定义EnvironmentPostProcessor是更好的选择,它让你拥有100%的控制权。
注意:无论选择哪种方案,加密密钥(或盐值、种子)的管理都是重中之重。绝对不要将密钥硬编码在业务代码或配置文件中。推荐的做法是使用环境变量(
JAVA_OPTS=-Djasypt.encryptor.password=你的密钥)或在应用启动时通过命令行参数传入。对于更高安全要求的场景,应该使用上述第4种方案,从安全的密钥服务中获取密钥。
2.2 与MyBatis-Plus的整合设计思路
MyBatis-Plus本身不提供也不关心配置加密,它只负责从Spring的Environment里读取spring.datasource下的配置来构建数据源。因此,我们的加密整合点必须在MyBatis-Plus(或者说Spring Boot的DataSourceAutoConfiguration)读取配置之前。
整个流程可以抽象为以下几步:
- 写入:开发/运维人员将加密后的字符串(如
3xY7vViQzB4K9hLgM2pNqRsT)写入配置文件,替换原来的明文密码。 - 加载:Spring Boot启动,加载配置文件到
Environment。 - 解密(关键钩子):在我们的自定义处理器(如
EnvironmentPostProcessor)中,遍历Environment的属性源,识别出需要解密的属性(例如,所有以.password结尾的key,或者被特定标记包裹的值),并调用解密服务进行解密。 - 替换:将解密后的明文值,写回
Environment。注意,这个过程是在内存中完成的,不会修改磁盘上的配置文件。 - 使用:MyBatis-Plus的自动配置类
DataSourceAutoConfiguration开始执行,它从Environment中读取spring.datasource.password,此时读到的已经是解密后的明文,从而成功创建出可用的DataSourceBean。
这样设计的好处是,对MyBatis-Plus零改造。它甚至不知道密码被加密过,整个加解密过程就像是一个安静的“前置过滤器”。
3. 基于自定义EnvironmentPostProcessor的实战实现
我更喜欢自定义EnvironmentPostProcessor的方案,因为它足够轻量、灵活,并且能清晰地展示整个解密流程。下面我们一步步来实现。
3.1 项目基础环境与依赖准备
首先,创建一个标准的Spring Boot项目,并引入MyBatis-Plus的依赖。
<!-- pom.xml --> <dependencies> <!-- Spring Boot Starter --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- MyBatis-Plus Starter --> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.6</version> <!-- 请使用最新稳定版 --> </dependency> <!-- 数据库驱动 (以MySQL为例) --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <!-- 加解密工具 (以Apache Commons Codec为例,简单演示) --> <dependency> <groupId>commons-codec</groupId> <artifactId>commons-codec</artifactId> <version>1.16.0</version> </dependency> <!-- 配置文件处理器,让IDE能识别自定义配置 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-configuration-processor</artifactId> <optional>true</optional> </dependency> </dependencies>这里没有引入Jasypt,因为我们准备自己实现解密器。我们使用commons-codec来做Base64编解码,实际加密算法你可以选择更安全的,比如AES。
3.2 实现核心解密工具类
我们先实现一个简单的AES加解密工具。再次强调,密钥secretKey绝不能写死在这里,我们稍后会从环境变量获取。
package com.example.demo.util; import org.apache.commons.codec.binary.Base64; import javax.crypto.Cipher; import javax.crypto.spec.SecretKeySpec; import java.nio.charset.StandardCharsets; /** * 简单的AES加解密工具类 (ECB模式,PKCS5Padding) * 注意:ECB模式不适合加密大量重复模式的数据,生产环境建议使用CBC或GCM模式并管理好IV。 */ public class AesUtil { // 算法定义 private static final String ALGORITHM = "AES"; private static final String TRANSFORMATION = "AES/ECB/PKCS5Padding"; /** * 加密 * @param content 明文 * @param secretKey 密钥(必须为16、24或32字节) * @return Base64编码的密文 */ public static String encrypt(String content, String secretKey) throws Exception { SecretKeySpec keySpec = new SecretKeySpec(secretKey.getBytes(StandardCharsets.UTF_8), ALGORITHM); Cipher cipher = Cipher.getInstance(TRANSFORMATION); cipher.init(Cipher.ENCRYPT_MODE, keySpec); byte[] encryptedBytes = cipher.doFinal(content.getBytes(StandardCharsets.UTF_8)); return Base64.encodeBase64String(encryptedBytes); } /** * 解密 * @param encryptedContent Base64编码的密文 * @param secretKey 密钥(必须为16、24或32字节) * @return 明文 */ public static String decrypt(String encryptedContent, String secretKey) throws Exception { SecretKeySpec keySpec = new SecretKeySpec(secretKey.getBytes(StandardCharsets.UTF_8), ALGORITHM); Cipher cipher = Cipher.getInstance(TRANSFORMATION); cipher.init(Cipher.DECRYPT_MODE, keySpec); byte[] decodedBytes = Base64.decodeBase64(encryptedContent); byte[] decryptedBytes = cipher.doFinal(decodedBytes); return new String(decryptedBytes, StandardCharsets.UTF_8); } }这个工具类提供了基础的AES加解密功能。为了便于在配置文件中识别哪些值需要解密,我们定义一个简单的格式约定,例如用DEC::作为密文的前缀。
3.3 实现自定义EnvironmentPostProcessor
这是最核心的一步。我们需要创建一个类实现EnvironmentPostProcessor接口,并在spring.factories中注册它。
package com.example.demo.config; import com.example.demo.util.AesUtil; import org.springframework.boot.SpringApplication; import org.springframework.boot.env.EnvironmentPostProcessor; import org.springframework.core.env.ConfigurableEnvironment; import org.springframework.core.env.MapPropertySource; import org.springframework.core.env.MutablePropertySources; import org.springframework.core.env.PropertySource; import org.springframework.util.StringUtils; import java.util.HashMap; import java.util.Map; public class DecryptEnvironmentPostProcessor implements EnvironmentPostProcessor { // 约定:密文以 "DEC::" 开头 private static final String PREFIX = "DEC::"; // 加密密钥的环境变量名 private static final String ENV_KEY = "CONFIG_ENCRYPT_KEY"; @Override public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) { // 1. 获取加密密钥 String secretKey = environment.getProperty(ENV_KEY); if (!StringUtils.hasText(secretKey)) { // 如果环境变量没有,尝试从命令行参数或默认配置获取(不推荐硬编码) secretKey = environment.getProperty("config.encrypt.key"); if (!StringUtils.hasText(secretKey)) { // 如果还是没有密钥,则不解密,但记录警告。生产环境应严格杜绝此情况。 System.err.println("警告: 未找到配置加密密钥,跳过配置文件解密。请设置环境变量 " + ENV_KEY); return; } } // 2. 获取所有的属性源 MutablePropertySources propertySources = environment.getPropertySources(); Map<String, Object> decryptedProperties = new HashMap<>(); // 3. 遍历所有属性源,寻找需要解密的属性 for (PropertySource<?> source : propertySources) { if (source instanceof EnumerablePropertySource) { EnumerablePropertySource<?> enumerableSource = (EnumerablePropertySource<?>) source; for (String propertyName : enumerableSource.getPropertyNames()) { Object propertyValue = source.getProperty(propertyName); if (propertyValue instanceof String) { String value = (String) propertyValue; // 判断是否需要解密 if (value.startsWith(PREFIX)) { String encryptedValue = value.substring(PREFIX.length()); try { String decryptedValue = AesUtil.decrypt(encryptedValue, secretKey); // 将解密后的键值对存入临时Map decryptedProperties.put(propertyName, decryptedValue); System.out.println("已解密配置项: " + propertyName); } catch (Exception e) { throw new RuntimeException("解密配置项 [" + propertyName + "] 失败,密文: " + encryptedValue, e); } } } } } } // 4. 将解密后的属性覆盖到环境变量中(最高优先级之一) if (!decryptedProperties.isEmpty()) { MapPropertySource decryptedPropertySource = new MapPropertySource("decryptedProperties", decryptedProperties); propertySources.addFirst(decryptedPropertySource); // 添加到最前面,确保优先级最高 } } }关键点解析:
- 密钥获取:首先从环境变量
CONFIG_ENCRYPT_KEY获取密钥,这是最安全的方式。如果没找到,可以有一个兜底(比如从config.encrypt.key读取),但生产环境必须使用环境变量。 - 遍历属性源:
Environment包含多个属性源(如命令行参数、系统属性、配置文件等)。我们遍历所有可枚举的源。 - 识别与解密:我们约定密文以
DEC::开头。识别到这样的值后,去掉前缀,调用AesUtil.decrypt进行解密。 - 覆盖属性:解密后的键值对存入一个
Map,并包装成一个新的MapPropertySource。通过addFirst()方法将其添加到属性源列表的最前面。这意味着,当Spring后续读取属性时,会优先使用我们解密后的值,覆盖掉原来DEC::开头的密文值。
3.4 注册Processor并编写配置文件
要让Spring Boot在启动时加载我们的EnvironmentPostProcessor,需要在resources/META-INF/下创建spring.factories文件。
# src/main/resources/META-INF/spring.factories org.springframework.boot.env.EnvironmentPostProcessor=com.example.demo.config.DecryptEnvironmentPostProcessor现在,我们来准备加密后的配置文件。首先,你需要用AesUtil.encrypt方法(可以写个简单的main方法或单元测试)将你的明文密码加密。
假设你的数据库明文密码是MySecretDBPassword123!,密钥是My16CharSecretKey!(16字节),加密后得到的Base64字符串可能是5f4dcc3b5aa765d61d8327deb882cf99(示例,非真实结果)。
那么,你的application.yml配置文件应该这样写:
# application.yml spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/my_db?useSSL=false&serverTimezone=UTC username: root password: DEC::5f4dcc3b5aa765d61d8327deb882cf99 # 这里是加密后的字符串,前面加上了我们约定的前缀 hikari: connection-timeout: 30000 maximum-pool-size: 10 # 其他配置... mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 开启SQL日志,方便调试3.5 启动应用并验证
最后一步就是启动应用了。密钥通过环境变量传入。
Linux/Mac:
export CONFIG_ENCRYPT_KEY=My16CharSecretKey! java -jar your-application.jarWindows (CMD):
set CONFIG_ENCRYPT_KEY=My16CharSecretKey! java -jar your-application.jar或者使用启动参数(安全性稍低,因为能在进程列表里看到):
java -DCONFIG_ENCRYPT_KEY=My16CharSecretKey! -jar your-application.jar启动时,你应该在日志中看到类似“已解密配置项: spring.datasource.password”的输出。随后,MyBatis-Plus会正常初始化,连接池成功建立,应用启动完成。你可以写一个简单的测试Controller或单元测试,注入DataSource并尝试获取一个连接,来验证配置是否真的解密成功了。
4. 方案优化、安全增强与生产级考量
上面的方案是一个可运行的原型,但要用于生产环境,还需要在安全性、健壮性和易用性上做大量增强。
4.1 加密算法的安全升级
我们之前使用了AES/ECB模式。ECB模式简单,但有个致命缺点:相同的明文块会加密成相同的密文块。如果密码很短,且配置文件中有多个相同密码,攻击者可能通过模式分析获得信息。
生产环境建议:
- 使用AES/CBC/PKCS5Padding或AES/GCM/NoPadding模式。CBC模式需要初始化向量(IV),GCM模式同时提供加密和完整性验证。
- IV的管理:IV不需要保密,但必须不可预测(通常随机生成)。对于配置文件加密,我们可以将IV和密文一起存储。例如,将
IV:密文拼接后Base64,再加上我们的DEC::前缀。 - 密钥长度:务必使用256位(32字节)的密钥。
- 考虑使用专业的库:比如Google的Tink库,它提供了更安全、易用的加密API,避免了自己实现可能产生的漏洞。
一个增强版的解密逻辑伪代码:
// 假设密文格式为 DEC::BASE64(IV + AES_CBC_ENCRYPT(明文)) String fullCipherText = value.substring(PREFIX.length()); byte[] cipherData = Base64.decode(fullCipherText); // 前16字节是IV byte[] iv = Arrays.copyOfRange(cipherData, 0, 16); // 后面是真正的密文 byte[] cipherText = Arrays.copyOfRange(cipherData, 16, cipherData.length); // 使用密钥和IV初始化Cipher进行解密...4.2 灵活的解密策略与属性匹配
我们之前只解密了以DEC::开头的值。但在实际中,可能需要更灵活的匹配规则:
- 基于属性名模式:解密所有名称中包含
password、secret、key、token的属性,无论其值格式如何。 - 多级前缀:可以设计不同的前缀对应不同的解密算法或密钥,例如
AES::、RSA::。 - 忽略某些属性源:可能不想解密来自命令行参数(
CommandLinePropertySource)的属性。
这需要我们在EnvironmentPostProcessor中实现更复杂的属性过滤和策略逻辑。
4.3 密钥管理:从环境变量到专业服务
环境变量是比代码硬编码好得多的方式,但仍有风险(例如,服务器被入侵后,环境变量可能被读取)。对于更高安全等级的要求:
- 启动脚本保护:确保存放启动脚本和设置环境变量的目录权限严格受限。
- 使用云厂商的密钥管理服务:如AWS KMS, Azure Key Vault, 阿里云KMS。应用启动时,通过实例角色(如AWS IAM Role)临时获取解密密钥,密钥不在任何持久化介质中留存。
- 集成HashiCorp Vault:Vault可以动态生成数据库凭证,应用根本不需要知道静态密码。这是最理想的“零密码”方案,但架构复杂度最高。
4.4 与配置中心结合
如果你的配置不是放在本地application.yml,而是来自Nacos、Apollo、Spring Cloud Config等配置中心,解密逻辑应该放在哪里?
- 客户端解密(推荐):配置中心下发热的密文配置,由客户端(你的Spring Boot应用)在接收到配置后,用自己的密钥解密。这要求每个客户端都拥有解密能力(密钥),但配置中心不感知加密。
- 服务端解密:配置中心服务端存储密文,并在有权限的客户端拉取时,由服务端解密后下发明文。这减轻了客户端负担,但将安全压力转移到了配置中心,且网络传输的是明文。
我们的EnvironmentPostProcessor方案天然支持客户端解密。只要配置中心客户端(如Nacos Client)将配置加载到Spring的Environment中,我们的Processor就能在Bean初始化前对其进行解密。
4.5 监控、日志与故障排查
- 谨慎日志:解密过程中,绝对不要在日志中打印解密后的明文密码。我们之前的
System.out.println仅用于调试,生产环境必须移除或改为DEBUG级别日志,且只打印属性名,不打印值。 - 健康检查:可以创建一个健康指示器(
HealthIndicator),在应用启动后尝试用解密后的配置建立一个简单的数据库连接,如果失败,则在健康检查端点中报告,便于运维发现配置错误。 - 清晰的错误信息:解密失败时(如密钥错误、密文格式错误),应抛出带有明确提示的异常,帮助快速定位问题,但不要泄露密钥或密文细节。
5. 常见问题、排查技巧与实操心得
在实际落地过程中,我踩过不少坑,这里总结一下最常见的问题和解决办法。
5.1 应用启动失败:DataSource创建异常
问题现象:应用启动时报错,提示“Cannot create connection to database server”或“Access denied for user”,但确认密码在加密前是正确的。
排查思路:
- 首先确认解密是否生效:在
DecryptEnvironmentPostProcessor中增加调试日志,打印出解密前后的属性名和值(值可打马赛克,如spring.datasource.password=DEC::****->spring.datasource.password=****[DECRYPTED])。确保Processor被正确加载和执行。 - 检查密钥:百分之八十的问题出在密钥上。确保环境变量
CONFIG_ENCRYPT_KEY已设置,且与加密时使用的密钥完全一致(注意大小写、空格、特殊字符)。 - 检查密文:确认配置文件中的密文是完整且未经篡改的Base64字符串。Base64字符串通常只包含
A-Za-z0-9+/=这些字符。可以尝试用在线工具(仅用于测试!)或写个小程序,用同一个密钥解密,看是否能得到预期明文。 - 检查依赖和算法:确保加解密双方使用的算法、模式、填充方式完全一致。例如,加密用
AES/CBC/PKCS5Padding,解密也必须用同样的。Cipher.getInstance(“AES”)在不同JDK版本或提供商下可能有默认模式,最好显式指定完整参数。
5.2 解密处理器未生效
问题现象:配置了DEC::前缀,但应用启动时没有解密日志,连接失败。
排查思路:
- 检查
spring.factories:确认文件路径是src/main/resources/META-INF/spring.factories,并且内容拼写正确,全类名无误。 - 检查Processor类是否在组件扫描路径:
EnvironmentPostProcessor的实现类不能被@Component等注解标记,也不应在主应用类的扫描包路径下被自动实例化。它必须通过spring.factories机制加载。如果被重复实例化,可能会导致问题。 - 检查属性源顺序:我们的Processor通过
addFirst添加解密后的属性源。如果其他后置的Processor或配置源修改了属性,可能会覆盖我们的结果。可以调试查看environment.getProperty(“spring.datasource.password”)最终的值是什么。
5.3 与特定配置或库的兼容性问题
问题现象:应用能启动,但某些功能(如Actuator端点、特定Starter)行为异常。
排查思路:
- 过早的解密:我们的Processor在
Environment准备阶段运行。如果某些库在这个阶段之前就需要读取配置(极其罕见),可能会读到密文。这种情况需要调整Processor的执行顺序(通过实现Ordered接口或使用@Order注解)。 - 属性名冲突:确保你的解密属性名不会和其他框架的预留属性名冲突。使用
DEC::前缀能很大程度上避免这个问题。 - Profile特异性配置:
application-dev.yml和application-prod.yml中的加密配置要分别处理。确保不同环境使用不同的密钥,或者密文是基于对应环境的正确明文加密的。
5.4 性能考量
加解密操作会有性能开销,但对于配置文件,这个开销是启动时一次性的,对运行时性能几乎没有影响。除非你有成百上千个需要解密的配置项,否则无需担心。如果确实很多,可以考虑缓存解密结果,避免在遍历属性源时重复解密同一个属性。
实操心得:
- 密钥轮换:定期更换加密密钥是一个好习惯。但这意味着你需要用新密钥重新加密所有配置文件,并协调应用重启。设计时要考虑好轮换流程。
- 备份明文:在安全的离线位置备份一份关键的明文配置(如数据库连接串),以防密钥丢失导致全线瘫痪。
- 循序渐进:可以先从最敏感的数据库密码开始加密,稳定后再逐步推广到Redis密码、API密钥等其他配置。
- 团队协作:在团队中推行此方案时,务必文档化加密/解密流程,并提供便捷的脚本工具,降低开发人员的操作门槛和出错概率。