文章目录
- [Spring Boot Starter 中间件账号密码加密方案设计与实现](#Spring Boot Starter 中间件账号密码加密方案设计与实现)
-
- 一、背景
- 二、设计目标
- 三、方案设计
-
- [3.1 密文格式](#3.1 密文格式)
- [3.2 密钥派生](#3.2 密钥派生)
- [3.3 加解密流程](#3.3 加解密流程)
- [3.4 密钥传递方式](#3.4 密钥传递方式)
- [3.5 安全警告](#3.5 安全警告)
- 四、核心代码实现
-
- [4.1 加解密工具类](#4.1 加解密工具类)
- [4.2 属性配置与密钥解析](#4.2 属性配置与密钥解析)
- [4.3 自动配置集成](#4.3 自动配置集成)
- 五、使用方式
-
- [5.1 加密密码](#5.1 加密密码)
- [5.2 配置应用](#5.2 配置应用)
- 六、安全性分析
-
- [6.1 威胁模型](#6.1 威胁模型)
- [6.2 局限性](#6.2 局限性)
- [6.3 与 Jasypt 对比](#6.3 与 Jasypt 对比)
- 七、总结
Spring Boot Starter 中间件账号密码加密方案设计与实现
在 Spring Boot 配置文件中,数据库密码、Redis 密码等敏感信息如何安全存储?本文介绍一种轻量级的 AES-256-GCM 加密方案,支持环境变量、JVM 参数、密钥文件三种安全的密钥传递方式,适用于 Starter 类库的场景。
一、背景
在开发 Spring Boot Starter 时,不可避免地要处理中间件连接信息。以我最近开源的一个 Snowflake ID 生成器 Starter 为例,它需要连接 Redis 或 MySQL 来完成 Worker ID 分配:
yaml
chenergao:
snowflake:
redis:
address: redis://localhost:6379
password: my-secret-password # 明文密码,有泄露风险
这种明文配置存在两个问题:
- 配置文件可能被提交到 Git:即使是私有仓库,一旦泄露后果严重
- 运维人员可以看到密码:配置中心、日志、审计系统中都可能暴露
因此需要一个配置加密方案,让配置文件中的密码以密文形式存储,应用启动时自动解密。
二、设计目标
在设计加密方案时,我遵循了几个核心原则:
- 不强制加密------明文密码依然可用,向下兼容
- 密钥不进配置文件------解密密钥不能和密文放在一起,否则等同明文
- 算法选标准方案------使用 NIST 推荐的 AES-256-GCM 认证加密
- 零外部依赖 ------仅使用 JDK 自带的
javax.crypto,不引入第三方加密库 - 接入成本低------对使用方来说,加一个环境变量 + 改一行配置即可
三、方案设计
3.1 密文格式
密文由固定前缀 ENC: + Base64 编码的密文组成:
ENC:<Base64(IV + Ciphertext)>
-
IV:12 字节随机初始化向量(每次加密生成不同的 IV)
-
Ciphertext:AES-256-GCM 加密后的密文(包含 128-bit 认证标签)
-
前缀
ENC::用于区分加密值和明文值,明文值没有此前缀,解密时直接透传明文: my-redis-password
密文: ENC:rK8vX2pQ...(Base64 编码)
3.2 密钥派生
用户提供的任意长度密钥通过 SHA-256 哈希,固定输出 32 字节(256-bit),作为 AES-256 的密钥:
User Key → SHA-256 → 256-bit AES Key
这样用户不需要记忆或生成正好 256-bit 的密钥,任意字符串都可以。
3.3 加解密流程
加密:
Plaintext → AES-256-GCM(Key, Random IV) → IV + Ciphertext → Base64 → "ENC:" + Base64
解密:
"ENC:" + Base64 → IV + Ciphertext → AES-256-GCM(Key, IV) → Plaintext
GCM 模式自带认证标签,解密时会校验密文完整性------密钥错误或被篡改的密文会直接抛出异常,应用启动失败,避免了静默使用错误密码的风险。
3.4 密钥传递方式
这是整个设计最关键的部分。Jasypt、Spring Cloud Config 等行业方案都有一个共同原则:解密密钥不能和密文放在同一个配置文件里。
我设计了三种安全的密钥传递方式,优先级从高到低:
| 优先级 | 方式 | 配置 | 适用场景 |
|---|---|---|---|
| 1 | 环境变量 | CHENERGAO_SNOWFLAKE_ENCRYPTION_KEY |
生产环境首选 |
| 2 | JVM 系统属性 | -Dchenergao.snowflake.encryption-key=... |
启动脚本 |
| 3 | 密钥文件 | chenergao.snowflake.encryption-key-file |
Docker/K8s 挂载 Secret |
环境变量方式(推荐):
bash
export CHENERGAO_SNOWFLAKE_ENCRYPTION_KEY=my-secret-key
JVM 系统属性:
bash
java -Dchenergao.snowflake.encryption-key=my-secret-key -jar app.jar
Kubernetes Secret 挂载文件:
yaml
# snowflake-key-secret.yaml
apiVersion: v1
kind: Secret
metadata:
name: snowflake-key
stringData:
key: "my-secret-key"
yaml
# application.yml
chenergao:
snowflake:
encryption-key-file: /run/secrets/snowflake-key
当密钥文件内容包含首尾空白或换行符时(Docker Secret 常见问题),会自动 trim 处理。
3.5 安全警告
如果用户将 encryption-key 写在 application.yml 中:
yaml
chenergao:
snowflake:
encryption-key: my-secret-key # ⚠️ 不安全的做法!
redis:
password: ENC:xxx...
启动时会检测密钥来源,发现来自配置文件则打印 WARN:
WARN SnowflakeProperties -- SECURITY: The 'encryption-key' property was found
in an application configuration file (applicationConfig: [classpath:/application.yml]).
This is insecure! Use the environment variable CHENERGAO_SNOWFLAKE_ENCRYPTION_KEY,
a JVM system property, or the 'encryption-key-file' property with a mounted secret
path instead.
四、核心代码实现
4.1 加解密工具类
java
public final class AesTextEncryptor {
private static final String ALGORITHM = "AES/GCM/NoPadding";
private static final int GCM_IV_LENGTH = 12; // 96-bit IV
private static final int GCM_TAG_LENGTH = 128; // 128-bit 认证标签
static final String PREFIX = "ENC:";
private final SecretKeySpec keySpec;
public AesTextEncryptor(String key) {
// SHA-256 派生 → 256-bit AES Key
MessageDigest sha256 = MessageDigest.getInstance("SHA-256");
byte[] keyBytes = sha256.digest(key.getBytes(StandardCharsets.UTF_8));
this.keySpec = new SecretKeySpec(keyBytes, "AES");
}
public String encrypt(String plaintext) {
byte[] iv = new byte[GCM_IV_LENGTH];
new SecureRandom().nextBytes(iv); // 随机 IV
Cipher cipher = Cipher.getInstance(ALGORITHM);
cipher.init(Cipher.ENCRYPT_MODE, keySpec, new GCMParameterSpec(GCM_TAG_LENGTH, iv));
byte[] ciphertext = cipher.doFinal(plaintext.getBytes(StandardCharsets.UTF_8));
// IV + Ciphertext → Base64
ByteBuffer buffer = ByteBuffer.allocate(GCM_IV_LENGTH + ciphertext.length);
buffer.put(iv);
buffer.put(ciphertext);
return PREFIX + Base64.getEncoder().encodeToString(buffer.array());
}
public String decrypt(String text) {
// 明文透传:不以 ENC: 开头的值直接返回
if (text == null || text.isEmpty() || !text.startsWith(PREFIX)) {
return text;
}
byte[] encrypted = Base64.getDecoder().decode(text.substring(PREFIX.length()));
ByteBuffer buffer = ByteBuffer.wrap(encrypted);
byte[] iv = new byte[GCM_IV_LENGTH];
buffer.get(iv);
byte[] ciphertext = new byte[buffer.remaining()];
buffer.get(ciphertext);
Cipher cipher = Cipher.getInstance(ALGORITHM);
cipher.init(Cipher.DECRYPT_MODE, keySpec, new GCMParameterSpec(GCM_TAG_LENGTH, iv));
return new String(cipher.doFinal(ciphertext), StandardCharsets.UTF_8);
}
// 判断值是否已加密
public static boolean isEncrypted(String text) {
return text != null && text.startsWith(PREFIX);
}
}
关键设计点:
decrypt()对明文透传 :不以ENC:开头的值原样返回。这是"不强制加密"的核心------已有的明文配置不受影响。- 每次加密使用随机 IV:同一个明文每次加密输出不同,防止模式分析。
- GCM 认证标签:解密时 Java 自动校验,密钥错误直接抛异常,fail-fast。
4.2 属性配置与密钥解析
java
@ConfigurationProperties(prefix = "chenergao.snowflake")
public class SnowflakeProperties implements EnvironmentAware {
private String encryptionKey; // 直接密钥(建议来自环境变量)
private String encryptionKeyFile; // 密钥文件路径(Docker/K8s 挂载)
private Environment environment;
public void decryptSensitiveFields() {
String key = resolveEncryptionKey();
if (key == null || key.isEmpty()) return;
AesTextEncryptor encryptor = new AesTextEncryptor(key);
redis.password = encryptor.decrypt(redis.password);
mysql.password = encryptor.decrypt(mysql.password);
}
private String resolveEncryptionKey() {
// 优先级 1: encryption-key(环境变量 > 系统属性 > 配置文件)
if (encryptionKey != null && !encryptionKey.isEmpty()) {
warnIfKeyInApplicationConfig(); // 检查是否来自配置文件
return encryptionKey;
}
// 优先级 2: encryption-key-file(从文件读取)
if (encryptionKeyFile != null && !encryptionKeyFile.isEmpty()) {
return readKeyFromFile(encryptionKeyFile);
}
// 都没配置则跳过解密
return null;
}
private void warnIfKeyInApplicationConfig() {
// 检查 Environment 中该属性是否来自 applicationConfig 源
if (environment instanceof AbstractEnvironment absEnv) {
for (PropertySource<?> ps : absEnv.getPropertySources()) {
if (ps.containsProperty("chenergao.snowflake.encryption-key")) {
if (ps.getName().contains("applicationConfig")) {
log.warn("SECURITY: encryption-key found in config file!");
}
break;
}
}
}
}
}
这里通过实现 EnvironmentAware 接口来访问 Spring 的 Environment,遍历 PropertySource 判断 encryption-key 是否来自 application.yml/application.properties,从而实现精准的安全警告。
4.3 自动配置集成
java
@Configuration
static class RedisConfiguration {
@Bean
public RedissonClient snowflakeRedissonClient(SnowflakeProperties properties) {
properties.decryptSensitiveFields(); // ← Bean 创建前先解密
return RedisSlotWorkerIdAllocator.buildRedissonClient(properties);
}
}
在 AutoConfiguration 的 @Bean 方法中,使用配置之前先调用 decryptSensitiveFields() 。因为解密是幂等的(解密后的明文不再以 ENC: 开头,二次调用透传),即使被多次调用也是安全的。
五、使用方式
5.1 加密密码
使用 Starter jar 自带的命令行工具:
bash
java -cp spring-boot-snowflake-starter-1.0.0.jar \
io.github.chenergao.snowflake.encrypt.AesTextEncryptor \
my-secret-key \
my-redis-password
# 输出: ENC:rK8vX2pQv7mN3w...
5.2 配置应用
生产环境(推荐环境变量):
bash
export CHENERGAO_SNOWFLAKE_ENCRYPTION_KEY=my-secret-key
yaml
# application.yml
chenergao:
snowflake:
redis:
password: ENC:rK8vX2pQv7mN3w...
Docker Compose:
yaml
services:
app:
environment:
CHENERGAO_SNOWFLAKE_ENCRYPTION_KEY: my-secret-key
Kubernetes:
yaml
apiVersion: v1
kind: Pod
spec:
containers:
- name: app
env:
- name: CHENERGAO_SNOWFLAKE_ENCRYPTION_KEY
valueFrom:
secretKeyRef:
name: snowflake-key
key: key
六、安全性分析
6.1 威胁模型
| 攻击场景 | 防护措施 |
|---|---|
| 配置文件泄露(Git、日志) | 密码以 ENC: 密文存储,无密钥无法解密 |
| 密钥和密文同文件 | 启动时检测并 WARN,引导用户修正 |
| 密钥暴力破解 | SHA-256 派生 + AES-256,暴力破解不可行 |
| 密文被篡改 | GCM 认证标签校验,篡改后解密失败 |
| CI/CD 流水线泄露 | 密钥通过 CI/CD Secret 变量注入,不进代码仓库 |
6.2 局限性
- 密钥在内存中是明文的------解密后密码以明文形式存在于 JVM 堆内存,可通过 heap dump 获取。这是所有配置加密方案的共性问题。
- 无法防止运维人员直接读取环境变量 ------有服务器登录权限的人仍可通过
env命令看到密钥。 - 适用场景是配置静态加密------对传输层、存储层的加密需求,需要配合 TLS、磁盘加密等其他机制。
6.3 与 Jasypt 对比
| 本方案 | Jasypt | |
|---|---|---|
| 加密算法 | AES-256-GCM | PBEWithHMACSHA512AndAES_256 |
| 外部依赖 | 无(JDK 标准库) | org.jasypt:jasypt |
| 密钥管理 | 环境变量/文件/系统属性 | 同 |
| 安全警告 | 配置文件检测 + WARN | 无 |
| 代码量 | ~100 行 | 依赖库 |
| 维护状态 | 自主可控 | 最后发布 2021 年 |
七、总结
这套方案的核心思路很简单:
- 用
ENC:前缀区分密文和明文------不强制加密,向下兼容 - 用 AES-256-GCM 做认证加密------标准算法,GCM 自带防篡改
- 密钥通过环境变量/文件传入------不进配置文件,避免密钥和密文同文件
- 实现
EnvironmentAware做来源检测------引导用户正确使用
不到 200 行代码,无外部依赖,适合不想引入 Jasypt 等重量级依赖、又想解决配置文件密码安全问题的场景。
本文对应的代码实现已开源:spring-boot-snowflake-starter