摘要: 在生产环境中,数据库密码、redis 密码、第三方 api 密钥等敏感信息如果以明文形式存储在配置文件中,将面临严重的安全风险。本文将深入探讨 springboot 配置文件的自动加解密方案,从原理到实战,带你构建一套安全、优雅的配置管理体系。
一、为什么需要配置文件加解密?
在日常开发中,我们的 application.yml 往往长这样:
spring:
datasource:
url: jdbc:mysql://localhost:3306/mydb
username: root
password: mys3cretp@ssw0rd!
redis:
host: 192.168.1.100
password: redisp@ss123
# 第三方服务密钥
aliyun:
oss:
access-key-id: ltai5txxxxxxxxxxxxxx
access-key-secret: hk7xxxxxxxxxxxxxxxxxxxxxxx明文配置的风险:
| 风险场景 | 说明 |
|---|---|
| 代码仓库泄露 | git 仓库被公开或内部人员不当访问,敏感信息一览无余 |
| 运维审计困难 | 无法追踪谁在何时查看或修改了敏感配置 |
| 合规要求 | 等保2.0、gdpr 等法规要求敏感数据必须加密存储 |
| 环境隔离不足 | 开发、测试、生产环境密码混用,增加泄露面 |
因此,我们需要一种方案:在配置文件中存储密文,应用启动时自动解密,对业务代码完全透明。
二、方案选型对比
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| jasypt spring boot | 开箱即用、社区成熟 | 密钥管理仍需注意 | 中小型项目 |
| 自定义 environmentpostprocessor | 灵活可控、无额外依赖 | 需要自行实现 | 有定制需求的项目 |
| spring cloud config + vault | 企业级、安全性极高 | 架构复杂、运维成本高 | 微服务/大型企业 |
| 自定义 propertysource | 轻量、可控 | 功能有限 | 简单加密需求 |
本文将重点讲解 jasypt 方案 和 自定义 environmentpostprocessor 方案,这两种方案覆盖了 90% 以上的实际使用场景。
三、jasypt spring boot(推荐入门)
3.1 引入依赖
<dependency>
<groupid>com.github.ulisesbocchio</groupid>
<artifactid>jasypt-spring-boot-starter</artifactid>
<version>3.0.5</version>
</dependency>3.2 配置加密密钥
在 application.yml 中配置加密密钥(master password):
jasypt:
encryptor:
algorithm: pbewithmd5anddes
iv-generator-classname: org.jasypt.iv.noivgenerator
password: ${jasypt_password:mydefaultmasterkey}重要提示: 生产环境中,jasypt.encryptor.password 不应硬编码在配置文件中,建议通过环境变量或 jvm 参数传入。
3.3 生成密文
编写一个工具类来生成加密后的配置值:
import org.jasypt.encryption.pbe.pooledpbestringencryptor;
import org.jasypt.encryption.pbe.config.simplestringpbeconfig;
public class jasyptencryptorutil {
private static final string algorithm = "pbewithmd5anddes";
private static final string master_password = "mydefaultmasterkey";
public static void main(string[] args) {
pooledpbestringencryptor encryptor = new pooledpbestringencryptor();
simplestringpbeconfig config = new simplestringpbeconfig();
config.setpassword(master_password);
config.setalgorithm(algorithm);
config.setkeyobtentioniterations("1000");
config.setpoolsize("1");
config.setprovidername("sunjce");
config.setsaltgeneratorclassname("org.jasypt.salt.randomsaltgenerator");
config.setivgeneratorclassname("org.jasypt.iv.noivgenerator");
config.setstringoutputtype("base64");
encryptor.setconfig(config);
// 加密
string plaintext = "mys3cretp@ssw0rd!";
string encrypted = encryptor.encrypt(plaintext);
system.out.println("明文: " + plaintext);
system.out.println("密文: " + encrypted);
// 解密验证
string decrypted = encryptor.decrypt(encrypted);
system.out.println("解密: " + decrypted);
}
}
运行后输出:
明文: mys3cretp@ssw0rd!
密文: ab3de5fg7hi9jk1lm2no4p==
解密: mys3cretp@ssw0rd!
3.4 在配置文件中使用密文
将生成的密文用 enc() 包裹,放入配置文件:
spring:
datasource:
url: jdbc:mysql://localhost:3306/mydb
username: root
password: enc(ab3de5fg7hi9jk1lm2no4p==)
redis:
host: 192.168.1.100
password: enc(xy9zw8vu7ts6rq5po4nm3l==)启动应用时传入密钥:
# 方式一:jvm 参数 java -jar app.jar --jasypt.encryptor.password=yourmasterkey # 方式二:环境变量 export jasypt_password=yourmasterkey java -jar app.jar # 方式三:系统属性 java -djasypt.encryptor.password=yourmasterkey -jar app.jar
3.5 jasypt 的自动解密原理
jasypt spring boot starter 的工作流程如下:
应用启动
│
▼
jasyptspringbootapplicationlistener 监听 applicationenvironmentpreparedevent
│
▼
遍历所有 propertysource
│
▼
发现值匹配 enc(xxx) 模式的属性
│
▼
使用配置的 encryptor 自动解密
│
▼
将解密后的明文替换回 propertysource
│
▼
业务代码通过 @value 或 environment 获取到的是明文
四、自定义 environmentpostprocessor(深度定制)
如果你的项目不想引入第三方依赖,或者有更复杂的加解密需求(如使用国密算法 sm4、对接 kms 密钥管理服务),可以通过自定义 environmentpostprocessor 来实现。
4.1 整体架构设计
┌─────────────────────────────────────────────────┐
│ application.yml │
│ spring.datasource.password=crypt:base64:xxx │
└────────────────────┬────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────┐
│ configdecryptenvironmentpostprocessor │
│ (实现 environmentpostprocessor 接口) │
│ │
│ 1. 遍历所有 propertysource │
│ 2. 识别 crypt: 前缀的配置项 │
│ 3. 调用 configdecryptor 解密 │
│ 4. 用解密后的值替换原 propertysource │
└────────────────────┬────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────┐
│ configdecryptor │
│ (策略接口,支持多种加密算法) │
│ ├── aesconfigdecryptor │
│ ├── sm4configdecryptor │
│ └── kmsconfigdecryptor │
└─────────────────────────────────────────────────┘
4.2 定义解密策略接口
/**
* 配置解密器策略接口
*/
public interface configdecryptor {
/**
* 判断是否支持该加密格式
*/
boolean supports(string encryptedvalue);
/**
* 执行解密
*/
string decrypt(string encryptedvalue);
}
4.3 实现 aes 解密器
import javax.crypto.cipher;
import javax.crypto.spec.secretkeyspec;
import java.util.base64;
/**
* 基于 aes 的配置解密器
*/
public class aesconfigdecryptor implements configdecryptor {
private static final string prefix = "crypt:aes:";
private static final string algorithm = "aes";
// 生产环境应从安全存储获取密钥
private final string secretkey;
public aesconfigdecryptor(string secretkey) {
// aes-128 需要 16 字节密钥
this.secretkey = padkey(secretkey, 16);
}
@override
public boolean supports(string encryptedvalue) {
return encryptedvalue != null && encryptedvalue.startswith(prefix);
}
@override
public string decrypt(string encryptedvalue) {
try {
string ciphertext = encryptedvalue.substring(prefix.length());
byte[] encrypted = base64.getdecoder().decode(ciphertext);
secretkeyspec keyspec = new secretkeyspec(secretkey.getbytes(), algorithm);
cipher cipher = cipher.getinstance(algorithm);
cipher.init(cipher.decrypt_mode, keyspec);
byte[] decrypted = cipher.dofinal(encrypted);
return new string(decrypted, "utf-8");
} catch (exception e) {
throw new runtimeexception("配置解密失败: " + encryptedvalue, e);
}
}
private string padkey(string key, int length) {
stringbuilder sb = new stringbuilder(key);
while (sb.length() < length) {
sb.append('0');
}
return sb.substring(0, length);
}
}
4.4 实现 environmentpostprocessor
import org.springframework.boot.springapplication;
import org.springframework.boot.env.environmentpostprocessor;
import org.springframework.core.env.configurableenvironment;
import org.springframework.core.env.enumerablepropertysource;
import org.springframework.core.env.mappropertysource;
import org.springframework.core.env.mutablepropertysources;
import org.springframework.core.env.propertysource;
import java.util.hashmap;
import java.util.list;
import java.util.map;
/**
* 配置文件自动解密后置处理器
* <p>
* 在 spring 环境准备完成后,自动扫描并解密配置中的加密属性
* </p>
*/
public class configdecryptenvironmentpostprocessor implements environmentpostprocessor {
private static final string decrypted_property_source_name = "decryptedconfig";
private final list<configdecryptor> decryptors;
public configdecryptenvironmentpostprocessor() {
// 从环境变量获取密钥,支持多种方式
string aeskey = system.getenv("config_aes_key");
if (aeskey == null) {
aeskey = system.getproperty("config.aes.key", "defaultaeskey1234");
}
this.decryptors = list.of(
new aesconfigdecryptor(aeskey)
// 可扩展: new sm4configdecryptor(...), new kmsconfigdecryptor(...)
);
}
@override
public void postprocessenvironment(configurableenvironment environment,
springapplication application) {
map<string, object> decryptedproperties = new hashmap<>();
mutablepropertysources propertysources = environment.getpropertysources();
// 遍历所有 propertysource
for (propertysource<?> propertysource : propertysources) {
if (!(propertysource instanceof enumerablepropertysource)) {
continue;
}
enumerablepropertysource<?> enumerablesource =
(enumerablepropertysource<?>) propertysource;
for (string propertyname : enumerablesource.getpropertynames()) {
object rawvalue = enumerablesource.getproperty(propertyname);
if (rawvalue instanceof string) {
string value = (string) rawvalue;
string decrypted = trydecrypt(value);
if (decrypted != null) {
decryptedproperties.put(propertyname, decrypted);
}
}
}
}
// 将解密后的属性添加到最高优先级,覆盖原始加密值
if (!decryptedproperties.isempty()) {
propertysources.addfirst(
new mappropertysource(decrypted_property_source_name, decryptedproperties)
);
}
}
/**
* 尝试使用所有注册的解密器进行解密
*/
private string trydecrypt(string value) {
for (configdecryptor decryptor : decryptors) {
if (decryptor.supports(value)) {
return decryptor.decrypt(value);
}
}
return null; // 不需要解密
}
}
4.5 注册 spi
创建文件 src/main/resources/meta-inf/spring.factories(spring boot 2.x):
org.springframework.boot.env.environmentpostprocessor=\ com.example.config.configdecryptenvironmentpostprocessor
如果是 spring boot 3.x,创建文件 src/main/resources/meta-inf/spring/org.springframework.boot.env.environmentpostprocessor.imports:
com.example.config.configdecryptenvironmentpostprocessor
4.6 使用加密配置
spring:
datasource:
url: jdbc:mysql://localhost:3306/mydb
username: root
# 使用 crypt:aes: 前缀标识需要解密的值
password: crypt:aes:ab3de5fg7hi9jk1lm2no4p==
redis:
password: crypt:aes:xy9zw8vu7ts6rq5po4nm3l==
# 自定义配置同样适用
aliyun:
oss:
access-key-secret: crypt:aes:pq3rs5tu7vw9xy1za2bc4d==启动应用:
# 通过环境变量传入 aes 密钥 export config_aes_key=yourproductionkey java -jar app.jar # 或通过 jvm 参数 java -dconfig.aes.key=yourproductionkey -jar app.jar
4.7 加密工具类
import javax.crypto.cipher;
import javax.crypto.spec.secretkeyspec;
import java.util.base64;
/**
* 配置加密工具 - 用于生成加密后的配置值
*/
public class configencryptutil {
private static final string algorithm = "aes";
public static string encrypt(string plaintext, string secretkey) throws exception {
string paddedkey = padkey(secretkey, 16);
secretkeyspec keyspec = new secretkeyspec(paddedkey.getbytes(), algorithm);
cipher cipher = cipher.getinstance(algorithm);
cipher.init(cipher.encrypt_mode, keyspec);
byte[] encrypted = cipher.dofinal(plaintext.getbytes("utf-8"));
return "crypt:aes:" + base64.getencoder().encodetostring(encrypted);
}
private static string padkey(string key, int length) {
stringbuilder sb = new stringbuilder(key);
while (sb.length() < length) {
sb.append('0');
}
return sb.substring(0, length);
}
public static void main(string[] args) throws exception {
string key = "yourproductionkey";
system.out.println(encrypt("mys3cretp@ssw0rd!", key));
// 输出: crypt:aes:xxxxxxxxxxxxxxxxxx==
system.out.println(encrypt("redisp@ss123", key));
// 输出: crypt:aes:yyyyyyyyyyyyyyyyyy==
}
}
五、结合 maven profile 实现多环境密钥隔离
在实际项目中,不同环境应使用不同的加密密钥:
5.1 maven 配置
<profiles>
<profile>
<id>dev</id>
<properties>
<config.master.key>devkey1234567890</config.master.key>
</properties>
</profile>
<profile>
<id>prod</id>
<properties>
<config.master.key>${env.prod_master_key}</config.master.key>
</properties>
</profile>
</profiles>5.2 在 application.yml 中引用
config:
encrypt:
master-key: @config.master.key@5.3 构建命令
# 开发环境 mvn clean package -p dev # 生产环境(需要设置环境变量 prod_master_key) mvn clean package -p prod
六、进阶:集成 kms 密钥管理服务
对于安全性要求极高的场景,可以将密钥托管到云厂商的 kms 服务:
/**
* 基于阿里云 kms 的配置解密器
*/
public class kmsconfigdecryptor implements configdecryptor {
private static final string prefix = "crypt:kms:";
private final kmsclient kmsclient;
public kmsconfigdecryptor(kmsclient kmsclient) {
this.kmsclient = kmsclient;
}
@override
public boolean supports(string encryptedvalue) {
return encryptedvalue != null && encryptedvalue.startswith(prefix);
}
@override
public string decrypt(string encryptedvalue) {
string ciphertext = encryptedvalue.substring(prefix.length());
decryptrequest request = new decryptrequest();
request.setciphertextblob(ciphertext);
decryptresponse response = kmsclient.decrypt(request);
return new string(
base64.getdecoder().decode(response.getplaintext())
);
}
}
配置文件中使用:
spring:
datasource:
password: crypt:kms:y2lwagvydgv4dc1mcm9tlwttcw==七、安全最佳实践
7.1 密钥管理清单
推荐做法:
- 通过环境变量注入密钥
- 使用 kms/vault 等密钥管理服务
- 不同环境使用不同密钥
- 定期轮换密钥
- 限制密钥的知悉范围
- 密钥与代码仓库完全隔离
避免做法:
- 密钥硬编码在代码中
- 密钥明文写在配置文件中
- 所有环境共用一个密钥
- 密钥通过聊天工具传递
- 密钥长期不更换
7.2 日志脱敏
即使配置已加密,也要注意日志中不要打印明文密码:
import ch.qos.logback.classic.pattern.messageconverter;
import ch.qos.logback.classic.spi.iloggingevent;
/**
* 日志脱敏转换器 - 自动屏蔽日志中的敏感信息
*/
public class sensitiveinfoconverter extends messageconverter {
private static final pattern password_pattern =
pattern.compile("(password|secret|key)\\s*[:=]\\s*\\s+", pattern.case_insensitive);
@override
public string convert(iloggingevent event) {
string message = event.getformattedmessage();
return password_pattern.matcher(message).replaceall("$1=******");
}
}
7.3 git 仓库安全
# .gitignore - 确保以下内容不被提交 *.env application-local.yml **/secrets/
配合 git-secrets 工具防止敏感信息意外提交:
# 安装 git-secrets brew install git-secrets # 配置禁止提交包含特定模式的文件 git secrets --add 'jasypt\.encryptor\.password\s*=\s*\s+' git secrets --add 'crypt:[a-z]+:[a-za-z0-9+/=]+'
八、方案对比总结
| 维度 | jasypt | 自定义 postprocessor | kms 集成 |
|---|---|---|---|
| 接入成本 | 极低 | 中等 | 较高 |
| 灵活性 | 一般 | 高 | 高 |
| 安全性 | 中等 | 中等 | 高 |
| 运维复杂度 | 低 | 低 | 高 |
| 算法可替换 | 有限 | 完全自由 | 完全自由 |
| 社区支持 | 成熟 | 自行维护 | 依赖云厂商 |
选型建议:
- 中小型项目 / 快速接入 → jasypt spring boot
- 有定制需求 / 国密合规 → 自定义 environmentpostprocessor
- 微服务架构 / 高安全要求 → kms + vault 集成
九、常见问题 faq
q1: 加密后的配置文件还能在 ide 中正常启动吗?
可以。只需在 ide 的 run configuration 中配置对应的环境变量或 vm options 即可:
vm options: -djasypt.encryptor.password=devkey123 environment variables: config_aes_key=devkey123
q2: 如何验证配置是否正确解密?
添加一个启动检查器:
@component
public class configdecryptverifier implements applicationrunner {
@value("${spring.datasource.password}")
private string datasourcepassword;
@override
public void run(applicationarguments args) {
if (datasourcepassword != null && datasourcepassword.startswith("crypt:")) {
throw new illegalstateexception("配置解密失败!请检查密钥配置。");
}
// 也可以检查是否仍然是 enc() 格式
if (datasourcepassword != null && datasourcepassword.startswith("enc(")) {
throw new illegalstateexception("jasypt 解密失败!请检查密钥配置。");
}
system.out.println("✅ 配置解密验证通过");
}
}
q3: 加密后 nacos/apollo 等配置中心还适用吗?
完全适用。environmentpostprocessor 在 spring 环境初始化后执行,无论是本地配置文件还是远程配置中心加载的属性,都会被扫描和解密。只需确保密文格式一致即可。
q4: 如何实现密钥的自动轮换?
支持"双密钥"过渡期:
@override
public string decrypt(string encryptedvalue) {
try {
// 先用新密钥解密
return decryptwithkey(encryptedvalue, newkey);
} catch (exception e) {
// 新密钥失败,回退到旧密钥
return decryptwithkey(encryptedvalue, oldkey);
}
}
到此这篇关于springboot中配置文件自动加解密方案的文章就介绍到这了,更多相关springboot配置文件加解密内容请搜索代码网以前的文章或继续浏览下面的相关文章希望大家以后多多支持代码网!
发表评论