Spring Boot Starter 中间件账号密码加密方案设计与实现

文章目录

  • [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   # 明文密码,有泄露风险

这种明文配置存在两个问题:

  1. 配置文件可能被提交到 Git:即使是私有仓库,一旦泄露后果严重
  2. 运维人员可以看到密码:配置中心、日志、审计系统中都可能暴露

因此需要一个配置加密方案,让配置文件中的密码以密文形式存储,应用启动时自动解密。

二、设计目标

在设计加密方案时,我遵循了几个核心原则:

  1. 不强制加密------明文密码依然可用,向下兼容
  2. 密钥不进配置文件------解密密钥不能和密文放在一起,否则等同明文
  3. 算法选标准方案------使用 NIST 推荐的 AES-256-GCM 认证加密
  4. 零外部依赖 ------仅使用 JDK 自带的 javax.crypto,不引入第三方加密库
  5. 接入成本低------对使用方来说,加一个环境变量 + 改一行配置即可

三、方案设计

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 局限性

  1. 密钥在内存中是明文的------解密后密码以明文形式存在于 JVM 堆内存,可通过 heap dump 获取。这是所有配置加密方案的共性问题。
  2. 无法防止运维人员直接读取环境变量 ------有服务器登录权限的人仍可通过 env 命令看到密钥。
  3. 适用场景是配置静态加密------对传输层、存储层的加密需求,需要配合 TLS、磁盘加密等其他机制。

6.3 与 Jasypt 对比

本方案 Jasypt
加密算法 AES-256-GCM PBEWithHMACSHA512AndAES_256
外部依赖 无(JDK 标准库) org.jasypt:jasypt
密钥管理 环境变量/文件/系统属性
安全警告 配置文件检测 + WARN
代码量 ~100 行 依赖库
维护状态 自主可控 最后发布 2021 年

七、总结

这套方案的核心思路很简单:

  1. ENC: 前缀区分密文和明文------不强制加密,向下兼容
  2. 用 AES-256-GCM 做认证加密------标准算法,GCM 自带防篡改
  3. 密钥通过环境变量/文件传入------不进配置文件,避免密钥和密文同文件
  4. 实现 EnvironmentAware 做来源检测------引导用户正确使用

不到 200 行代码,无外部依赖,适合不想引入 Jasypt 等重量级依赖、又想解决配置文件密码安全问题的场景。

本文对应的代码实现已开源:spring-boot-snowflake-starter

相关推荐
摇滚侠1 小时前
Maven 依赖范围
java·maven
AKA__Zas1 小时前
芝士算法(滑动窗口片 2.0)
java·算法·leetcode·学习方法
Zella折耳根4 小时前
复习篇-常用实用类
java
devilnumber9 小时前
Java 递归算法 详解 + 核心要点 + 实战运用 + 避坑指南
java·开发语言·算法
独泪了无痕10 小时前
MyBatis魔法堂:结果集映射
后端·mybatis
copyer_xyf10 小时前
LangChain 调用 LLM
后端·python·agent
copyer_xyf11 小时前
Prompt 组织管理
后端·python·agent
asdfg125896311 小时前
JavaBean是什么?怎么理解?有什么用途?
java·开发语言
摇滚侠12 小时前
SpringMVC 入门到实战 文件上传 75-77
java·后端·spring·maven·intellij-idea