在Spring Boot项目中, 可以结合Jasypt 快速实现对配置文件中的部分属性进行加密。 完整的介绍参照:
Spring Boot + Jasypt 实现application.yml 属性加密的快速示例
但是作为一个技术强迫症,总是想着从底层开始实现属性的加解密,以解答这背后机制的疑惑。
本篇在不使用Jasypt 的状况下,实现配置文件的加密,并且应用启动的时候自动解密加密属性以供系统使用。
1. 定义一个加解密的工具类
/**
* Copyright (C) Oscar Chen(XM):
*
* Date: 2025-01-07
* Author: XM
*/
package com.osxm.sb.encyproperties.util;
import java.security.MessageDigest;
import java.security.SecureRandom;
import java.util.Arrays;
import java.util.Base64;
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
public class SecurityUtil {
private static final String AES_GCM_NOPADDING = "AES/GCM/NoPadding";
private static final int GCM_TAG_LENGTH = 128;
private static final int GCM_IV_LENGTH = 12;
public static SecretKey generateKeyByPassword(String password) throws Exception {
// 使用SHA-256哈希函数
MessageDigest sha = MessageDigest.getInstance("SHA-256");
byte[] keyBytes = sha.digest(password.getBytes("UTF-8"));
// AES-256需要一个长度为256位的密钥,所以取哈希的前32字节
keyBytes = Arrays.copyOf(keyBytes, 32);
// 创建密钥
SecretKey secretKey = new SecretKeySpec(keyBytes, "AES");
return secretKey;
}
public static String encrypt(String data, String password) throws Exception {
SecretKey key = generateKeyByPassword(password);
return encrypt(data, key);
}
public static String encrypt(String data, SecretKey key) throws Exception {
Cipher cipher = Cipher.getInstance(AES_GCM_NOPADDING);
byte[] iv = new byte[GCM_IV_LENGTH];
SecureRandom.getInstanceStrong().nextBytes(iv);
GCMParameterSpec gcmParameterSpec = new GCMParameterSpec(GCM_TAG_LENGTH, iv);
cipher.init(Cipher.ENCRYPT_MODE, key, gcmParameterSpec);
byte[] encryptedData = cipher.doFinal(data.getBytes());
byte[] encryptedDataWithIv = new byte[iv.length + encryptedData.length];
System.arraycopy(iv, 0, encryptedDataWithIv, 0, iv.length);
System.arraycopy(encryptedData, 0, encryptedDataWithIv, iv.length, encryptedData.length);
return Base64.getEncoder().encodeToString(encryptedDataWithIv);
}
public static String decrypt(String data, String password) throws Exception {
SecretKey key = generateKeyByPassword(password);
return decrypt(data, key);
}
public static String decrypt(String encryptedDataWithIv, SecretKey key) throws Exception {
byte[] decodedData = Base64.getDecoder().decode(encryptedDataWithIv);
Cipher cipher = Cipher.getInstance(AES_GCM_NOPADDING);
byte[] iv = new byte[GCM_IV_LENGTH];
System.arraycopy(decodedData, 0, iv, 0, iv.length);
GCMParameterSpec gcmParameterSpec = new GCMParameterSpec(GCM_TAG_LENGTH, iv);
cipher.init(Cipher.DECRYPT_MODE, key, gcmParameterSpec);
byte[] encryptedData = new byte[decodedData.length - iv.length];
System.arraycopy(decodedData, iv.length, encryptedData, 0, encryptedData.length);
byte[] decryptedData = cipher.doFinal(encryptedData);
return new String(decryptedData);
}
}
2. 定义一个继承EnumerablePropertySource的类: EncryptedPropertySource
EncryptedPropertySource
用于将数据库密码、API密钥等敏感配置信息以加密的形式存储。这样,即使配置文件(如.properties
文件或环境变量)被未经授权的人员访问,他们也无法直接获取到敏感的明文信息,因为信息已经被加密处理。
/**
* Copyright (C) Oscar Chen(XM):
*
* Date: 2025-01-07
* Author: XM
*/
package com.osxm.sb.encyproperties.config;
import java.util.HashMap;
import java.util.Map;
import org.springframework.core.env.EnumerablePropertySource;
import com.osxm.sb.encyproperties.util.SecurityUtil;
public class EncryptedPropertySource extends EnumerablePropertySource<Map<String, Object>> {
private final Map<String, Object> properties = new HashMap<>();
public EncryptedPropertySource(String name, Map<String, Object> source) {
super(name, source);
source.forEach((key, value) -> {
if (value != null) {
try {
properties.put(key, decrypt(value.toString()));
} catch (Exception e) {
properties.put(key, value);
}
}
});
}
@Override
public String[] getPropertyNames() {
return properties.keySet().toArray(new String[0]);
}
@Override
public Object getProperty(String name) {
return properties.get(name);
}
private String decrypt(String encryptedValue) throws Exception {
// 在这里实现你的解密逻辑
// 例如,假设加密值是 "ENC(encryptedPassword)" 格式
if (encryptedValue.startsWith("ENC(") && encryptedValue.endsWith(")")) {
String encryptedPassword = encryptedValue.substring(4, encryptedValue.length() - 1);
// 这里使用简单的Base64解码作为示例,你可以使用更复杂的解密算法
return SecurityUtil.decrypt(encryptedPassword, "oscar");
}
return encryptedValue;
}
}
3. 在启动类中添加初始化动作
在 Spring Boot 中,addInitializers
方法通常用于向ConfigurableApplicationContext
添加自定义的ApplicationContextInitializer
。ApplicationContextInitializer
是一个接口,允许你在ApplicationContext
刷新之前进行一些初始化工作。这在一些高级配置和启动时设置环境变量或属性时非常有用。
在Spring Boot中,environment
对象通常指的是Environment
接口的一个实例,它提供了访问应用程序属性源(PropertySource
)的方法。PropertySource
是一个抽象,它定义了如何访问一组属性(键值对)。
environment.getPropertySources().addFirst(...)
这行代码的作用是将一个新的PropertySource
实例添加到当前Environment
的属性源列表的最前面。这意味着当Spring Boot查找某个属性时,它会首先在这个新添加的PropertySource
中查找,如果找不到,才会继续在其他属性源中查找。
这种方法通常用于覆盖或添加额外的配置属性,特别是在需要确保某些属性具有最高优先级时。例如,你可能希望在应用程序启动时从外部源(如环境变量、命令行参数或加密的配置服务)加载配置,并确保这些配置覆盖任何默认或先前加载的配置。
这里的完整启动类代码如下:
@SpringBootApplication
public class EncypropertiesApplication {
public static void main(String[] args) {
SpringApplication application = new SpringApplication(EncypropertiesApplication.class);
application.addInitializers(new ApplicationContextInitializer<ConfigurableApplicationContext>() {
@Override
public void initialize(ConfigurableApplicationContext applicationContext) {
ConfigurableEnvironment environment = applicationContext.getEnvironment();
try (InputStream input = new ClassPathResource("application.yml").getInputStream()) {
Yaml yaml = new Yaml();
Map<String, Object> yamlProperties = yaml.load(input);
Map<String, Object> flattenedProperties = new HashMap<>();
flattenMap(yamlProperties, flattenedProperties, null);
EncryptedPropertySource encryptedPropertySource = new EncryptedPropertySource("encryptedProperties",
flattenedProperties);
environment.getPropertySources().addFirst(encryptedPropertySource);
} catch (IOException e) {
e.printStackTrace();
}
}
private void flattenMap(Map<String, Object> source, Map<String, Object> target, String path) {
source.forEach((key, value) -> {
String fullPath = (path == null ? key : path + "." + key);
if (value instanceof Map) {
flattenMap((Map<String, Object>) value, target, fullPath);
} else {
target.put(fullPath, value);
}
});
}
});
application.run(args);
}
}
4. 验证
以上配置是否生效,可以通过Debug 模式调试加密字符串是否正常被解密。
完整的测试是在 application.yml 配置数据库的密码,整合起来验证是否能正确连接DB。
spring:
application:
name: encyproperties
datasource:
url: jdbc:mysql://localhost/osxmdb
username: root
password: ENC(gfsTDJv1lgAfp4q0XcrhMp3+ijj8T2Go/UPdYbPJPXa6PQ==)
driver-class-name: com.mysql.cj.jdbc.Driver
本篇完整示例
- https://github.com/osxm/springbootency/tree/main/encyproperties