Spring Boot配置中心敏感数据加密解决方案
在微服务架构中,配置中心往往存储着数据库密码、API密钥等敏感信息。
本文介绍两种Spring Boot环境下的配置加密方案。
1)使用jasypt-spring-boot-starter方案
2)基于EnvironmentPostProcessor自定义加解密方案
方案一:使用jasypt-spring-boot-starter
实现步骤
1.添加依赖
<dependency>
<groupId>com.github.ulisesbocchio</groupId>
<artifactId>jasypt-spring-boot-starter</artifactId>
<version>3.0.5</version>
</dependency>
2.添加properties/yml配置项
jasypt.encryptor.bootstrap=true
jasypt.encryptor.password=d9d41b8469A-ca4b3db8caS15f28065e55d
jasypt.encryptor.algorithm=PBEWithMD5AndDES
jasypt.encryptor.salt-generator-classname=org.jasypt.salt.RandomSaltGenerator
jasypt.encryptor.iv-generator-classname=org.jasypt.iv.RandomIvGenerator
注:password在生产环境需要单独配置,可以单独指定生产的classpath,或在运行时通过-D参数动态指定passowrd
3.使用
import javax.annotation.PostConstruct;
import org.jasypt.encryption.StringEncryptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@RestController
@RequestMapping("/test")
public class TestController {
@Value("${spring.datasource.password}")
private String password;
@Autowired
private StringEncryptor encryptor;
@GetMapping("/encrypt")
public Object encrypt(@RequestParam("data") String data) {
return encryptor.encrypt(data);
}
@PostConstruct
public void init() {
log.info("password:{}", password);
}
}
调用encrypt接口,获取密文,将密文配置在配置文件中,例如ENC(密文),应用在启动时会自动解密。
优势
- 开箱即用:无需编写加解密逻辑
- 标准化:支持多种加密算法(PBE、AES等)
- 生态完善:与Spring Cloud Config无缝集成
- 安全可靠:使用随机盐值加密,相同明文每次加密结果不同
方案二:自定义BeanFactoryPostProcessor
Spring Boot在启动时会加载各种PropertySource,包括本地配置文件、环境变量、命令行参数等,大部分配置中心都是通过实现自己的EnvironmentPostProcessor或ApplicationContextInitializer,并在其中添加自己的PropertySource,所以如果需要自己来实现应用启动自动解密,必须要在配置中心将配置文件加载之后,我们才能对加载进来的property进行二次迭代。
代码
import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.CompositePropertySource;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.EnumerablePropertySource;
import org.springframework.core.env.MapPropertySource;
import org.springframework.core.env.PropertySource;
import org.springframework.util.Base64Utils;
/**
* 读取环境变量,将需要解密的属性进行解密。
* 使用方法如下
* 1. 使用当前文件的Security类的encrypt方法对敏感数据进行加密后,将加密后的密文配置到配置文件中,格式为PV(密文)
* 2. 加密的密钥,可以在运行时设置java环境变量enc.key.data=x, 若不设置默认使用default_key_data
*/
@Configuration
public class ConfigEnvironmentPostProcessor implements BeanFactoryPostProcessor {
private static final String PREFIX = "PV(";
private static final String SUFFIX = ")";
private static final String DECRYPTED_PROPERTY_SOURCE_PREFIX = "decrypted-";
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) {
ConfigurableEnvironment environment = beanFactory.getBean(ConfigurableEnvironment.class);
decryptEnvironment(environment);
}
private void decryptEnvironment(ConfigurableEnvironment environment) {
List<PropertySource<?>> sources = new ArrayList<>();
environment.getPropertySources().forEach(sources::add);
for (PropertySource<?> source : sources) {
Map<String, Object> decryptedProps = new HashMap<>();
collectDecryptedProperties(source, decryptedProps);
if (!decryptedProps.isEmpty()) {
environment.getPropertySources().addFirst(
new MapPropertySource(DECRYPTED_PROPERTY_SOURCE_PREFIX + source.getName(), decryptedProps)
);
}
}
}
private void collectDecryptedProperties(PropertySource<?> source,
Map<String, Object> decryptedProps) {
if (source instanceof CompositePropertySource) {
((CompositePropertySource) source).getPropertySources()
.forEach(s -> collectDecryptedProperties(s, decryptedProps));
} else if (source instanceof EnumerablePropertySource) {
EnumerablePropertySource<?> enumerable = (EnumerablePropertySource<?>) source;
Arrays.stream(enumerable.getPropertyNames())
.filter(key -> source.getProperty(key) instanceof String)
.forEach(key -> {
String value = (String) source.getProperty(key);
if (value.startsWith(PREFIX) && value.endsWith(SUFFIX)) {
String strValue = value.substring(PREFIX.length(), value.length() - SUFFIX.length());
// 可以换成自己的加解密工具
decryptedProps.put(key, Security.decrypt(strValue));
}
});
}
}
public static class Security {
private static final String static_version = "P";
private static final String key_data = "enc.key.data";
private static final String default_key_data = "qazwsx";
public static String decrypt(String data) {
try {
data = secDecrypt(data, getKeyData());
} catch (Exception e) {
e.printStackTrace();
}
return data;
}
private static String encrypt(String data) {
try {
data = secEncrypt(data, getKeyData());
} catch (Exception e) {
e.printStackTrace();
}
return data;
}
private static byte[] getKeyData() {
String property = System.getProperty(key_data);
if (StringUtils.isBlank(property)) {
return default_key_data.getBytes();
}
return property.getBytes();
}
private static String secDecrypt(String encryptStr, byte[] pkey) throws Exception {
if (StringUtils.isBlank(encryptStr)) {
return null;
}
int index = encryptStr.indexOf("-");
String tempStr = encryptStr;
if (index > 0) {
tempStr = encryptStr.substring(index + 1);
}
return aesDecryptByBytes(Base64Utils.decode(tempStr.getBytes()), pkey);
}
private static String aesDecryptByBytes(byte[] encryptBytes, byte[] encryptKey) throws Exception {
KeyGenerator kgen = KeyGenerator.getInstance("AES");
SecureRandom random = SecureRandom.getInstance("SHA1PRNG");
random.setSeed(encryptKey);
kgen.init(128, random);
SecretKey secretKey = new SecretKeySpec(kgen.generateKey().getEncoded(), "AES");
Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5PADDING");
cipher.init(Cipher.DECRYPT_MODE, secretKey);
byte[] decryptBytes = cipher.doFinal(encryptBytes);
return new String(decryptBytes);
}
private static String secEncrypt(String content, byte[] pkey) throws Exception {
if (StringUtils.isBlank(content)) {
return null;
}
return static_version + "-" + Base64Utils.encodeToString(aesEncryptToBytes(content, pkey));
}
private static byte[] aesEncryptToBytes(String content, byte[] encryptKey) throws Exception {
KeyGenerator kgen = KeyGenerator.getInstance("AES");
SecureRandom random = SecureRandom.getInstance("SHA1PRNG");
random.setSeed(encryptKey);
kgen.init(128, random);
SecretKey secretKey = new SecretKeySpec(kgen.generateKey().getEncoded(), "AES");
Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5PADDING");
cipher.init(Cipher.ENCRYPT_MODE, secretKey);
return cipher.doFinal(content.getBytes("UTF-8"));
}
public static void main(String[] args) {
//String decryptResult = Security.decrypt("密文");
//System.out.println("解密结果:" + decryptResult);
// 输入明文
String encryptResult = Security.encrypt("明文");
// 将输出的结果放入配置文件
System.out.println("加密结果:" + encryptResult);
}
}
}
该类实现了BeanFactoryPostProcessor,会在BeanDefinition加载之后,Bean实例化之前执行,在这时所有的配置文件均已加载完毕。可以根据environment.getPropertySources()获取到所有的配置文件,在逐一进行遍历,依据前缀和后缀的标识决定是否进行解密操作。
优势
- 完全可控:可自定义加密算法(示例使用AES)
- 灵活标记:支持PV()前缀等自定义标识
- 深度集成:可在属性加载阶段进行干预
劣势
- 实现复杂:需处理PropertySource的遍历逻辑
- 维护成本:需要自行保证加密实现的安全性
- 兼容性问题:对CompositePropertySource需要特殊处理
方案对比
| 维度 | jasypt方案 | 自定义方案 |
|---|---|---|
| 开发效率 | ⭐⭐⭐⭐⭐(快速集成) | ⭐⭐⭐⭐⭐(快速集成) |
| 安全性 | ⭐⭐⭐⭐(社区验证) | ⭐⭐(依赖自身实现) |
| 可维护性 | ⭐⭐⭐⭐(标准方案) | ⭐(需持续维护) |
| 算法扩展性 | ⭐⭐(配置修改) | ⭐⭐⭐⭐(代码级修改) |
| 与配置中心耦合度 | ⭐(无特殊处理) | ⭐⭐⭐(可深度定制) |
选型建议
选择jasypt方案:
- 需要快速落地加密方案
- 项目组没有密码学专家
- 使用标准加密算法即可满足需求
选择自定义方案:
- 需要与公司安全规范对齐
- 有特殊加密算法要求
- 需要深度定制属性加载逻辑
- 配置中心有特殊的加密格式要求
提醒
使用时,无论是哪种方案,在生产环境上,都不建议写死加解密的密钥,可以参考如下配置。
1.运行时通过-D参数动态指定
2.直接指定classpath,将配置文件单独在服务器存储
3.修改源代码,将读取密钥的逻辑改为从指定目录的文件中获取
4.使用HSM(Hardware Security Module)硬件加密模块处理密钥

837

被折叠的 条评论
为什么被折叠?



