前言

在企业级 SpringBoot 项目中,配置文件(application.yml)往往存储着大量敏感信息(比如:数据库密码、Redis 密码、MQ 账号密码、第三方密钥等)。如果明文存储,一旦配置文件泄露,会直接导致核心服务被入侵,属于等保、密评、PIPL 合规高风险项。因此,配置文件敏感信息必须加密,且生产环境必须使用国密 SM4 算法,满足国产化、合规要求。

本文基于 Java8 + SpringBoot2.3.7,采用纯工具类方式实现国密 SM4 配置加密解密。工具类方式:

  1. 无需启动 Spring 容器,静态调用即可使用;
  2. 自带 main 方法,本地直接运行生成密文、解密测试;
  3. 支持单层 / 多层配置读取解析;
  4. 自动识别 ENC(加密串) 格式,无感自动解密;
  5. 无侵入、轻量、可直接集成到现有项目。

一、核心依赖

项目依赖 Hutool 国密工具包 + BouncyCastle 加密库:

<!-- Hutool 国密加密(SM4) -->
<dependency>
    <groupId>cn.hutool</groupId>
    <artifactId>hutool-crypto</artifactId>
    <version>5.8.20</version>
</dependency>

<!-- BouncyCastle 密码学提供方 -->
<dependency>
    <groupId>org.bouncycastle</groupId>
    <artifactId>bcprov-jdk15on</artifactId>
    <version>1.70</version>
</dependency>

二、YML 配置文件(application.yml)

密钥规则:SM4 密钥必须是 16 位字符(数字 / 字母均可),生产环境严禁硬编码在配置文件,建议放到 Nacos / 密钥管理系统。加密格式固定:ENC(SM4 加密后的密文)

# ====================== 国密 SM4 配置 ======================
sm4:
  key: 1234567890123456  # 必须16位,测试使用,生产环境请放配置中心

# ====================== 测试加密配置 ======================
## 单层配置
testPassword: ENC(1bb11e4cf9eebd2538d53ebcaacb9cfe)
## 多层配置
test:
  password: ENC(1bb11e4cf9eebd2538d53ebcaacb9cfe)

三、国密 SM4 加密解密工具类(Sm4Utils)

import cn.hutool.crypto.SmUtil;
import cn.hutool.crypto.symmetric.SM4;

/**
 * 国密 SM4 对称加密工具类
 * 提供明文加密、密文解密 (本地可直接运行生成配置文件密文)
 *
 */
public class Sm4Utils {

    /**
     * 从配置文件读取 SM4 密钥(16位)
     * 生产环境:密钥不要硬编码,建议从配置中心/密钥系统获取
     */
    private static final String SM4_KEY = ApplicationConfigUtils.getProperty("sm4.key");

    /**
     * 初始化 SM4 加密对象(全局单例,避免重复创建)
     */
    private static final SM4 SM4_INSTANCE = SmUtil.sm4(SM4_KEY.getBytes());

    /**
     * SM4 加密(明文 → 16进制密文)
     * @param plainText 明文字符串
     * @return 可直接放入 ENC() 的密文
     */
    public static String encrypt(String plainText) {
        if (plainText == null || plainText.isEmpty()) {
            throw new IllegalArgumentException("加密明文不能为空");
        }
        return SM4_INSTANCE.encryptHex(plainText);
    }

    /**
     * SM4 解密(16进制密文 → 明文)
     * @param cipherText 去除 ENC() 后的密文
     * @return 原始明文
     */
    public static String decrypt(String cipherText) {
        if (cipherText == null || cipherText.isEmpty()) {
            throw new IllegalArgumentException("解密密文不能为空");
        }
        return SM4_INSTANCE.decryptStr(cipherText);
    }

    public static void main(String[] args) {
        // 待加密的明文(如数据库密码、Redis密码)
        String originalData = "123456";
        System.out.println("=== SM4 加解密测试 ===");
        System.out.println("加密前明文:" + originalData);

        // 加密(结果直接复制到配置文件 ENC() 中)
        String encryptData = Sm4Utils.encrypt(originalData);
        System.out.println("加密后密文:" + encryptData);

        // 解密验证
        String decryptData = Sm4Utils.decrypt(encryptData);
        System.out.println("解密后明文:" + decryptData);
    }
}

四、配置文件读取解密工具类(ApplicationConfigUtils)

无需启动 Spring,可静态直接调用,自动识别 ENC(xxx) 并解密,支持单层 / 多层配置。

/**
 * SpringBoot 配置文件读取工具类
 * 无需启动Spring容器,读取 application.yml 并自动解密 ENC(xxx)
 * 支持单层、多层级配置读取 + 国密 SM4 自动解密
 *
 */
public class ApplicationConfigUtils {

    /** YAML 解析器 */
    private static final Yaml YAML = new Yaml();

    /** 配置文件缓存 Map */
    private static final Map<String, Object> YML_CONFIG_MAP;

    /** 加密内容标识前缀、后缀 */
    private static final String ENCRYPT_PREFIX = "ENC(";
    private static final String ENCRYPT_SUFFIX = ")";

    // 静态代码块:项目启动时一次性加载配置文件到内存
    static {
        try {
            // 读取 resources 下的 application.yml
            Resource resource = new ClassPathResource("application.yml");
            if (!resource.exists()) {
                throw new RuntimeException("未找到配置文件:application.yml,请检查路径");
            }

            try (InputStream inputStream = resource.getInputStream()) {
                YML_CONFIG_MAP = YAML.load(inputStream);
            }

            if (YML_CONFIG_MAP == null || YML_CONFIG_MAP.isEmpty()) {
                throw new RuntimeException("application.yml 配置文件内容为空");
            }

        } catch (Exception e) {
            throw new RuntimeException("【配置加载失败】读取 application.yml 异常", e);
        }
    }

    /**
     * 读取配置(自动解密 ENC 格式)
     * @param key 配置项(如:test.password、testPassword)
     * @return 解密后的真实值
     */
    public static String getProperty(String key) {
        if (key == null || key.isBlank()) {
            throw new IllegalArgumentException("配置项 key 不能为空");
        }

        String[] keys = key.split("\\.");
        Object value = getNestedValue(YML_CONFIG_MAP, keys, 0);

        // 自动识别并解密
        return autoDecrypt(value);
    }

    /**
     * 读取配置(带默认值,避免报错)
     */
    public static String getProperty(String key, String defaultValue) {
        try {
            return getProperty(key);
        } catch (Exception e) {
            return defaultValue;
        }
    }

    /**
     * 递归获取多层配置值(如 a.b.c)
     */
    private static Object getNestedValue(Map<String, Object> currentMap, String[] keys, int index) {
        if (index == keys.length - 1) {
            return currentMap.get(keys[index]);
        }

        Object nextNode = currentMap.get(keys[index]);
        if (!(nextNode instanceof Map)) {
            throw new RuntimeException("配置节点不存在:" + keys[index]);
        }

        return getNestedValue((Map<String, Object>) nextNode, keys, index + 1);
    }

    /**
     * 自动解密(判断是否为 ENC(xxx) 格式,是则解密,否则直接返回)
     */
    private static String autoDecrypt(Object value) {
        if (value == null) {
            return null;
        }

        String strValue = String.valueOf(value);
        // 匹配加密格式,自动解密
        if (strValue.startsWith(ENCRYPT_PREFIX) && strValue.endsWith(ENCRYPT_SUFFIX)) {
            String cipherText = strValue.substring(ENCRYPT_PREFIX.length(), strValue.length() - ENCRYPT_SUFFIX.length());
            return Sm4Utils.decrypt(cipherText);
        }

        // 非加密格式直接返回
        return strValue;
    }

    public static void main(String[] args) {
        System.out.println("=== 配置文件解密测试 ===");
        // 单层配置
        String pwd1 = getProperty("testPassword");
        System.out.println("单层配置 testPassword 解密结果:" + pwd1);

        // 多层配置
        String pwd2 = getProperty("test.password");
        System.out.println("多层配置 test.password 解密结果:" + pwd2);
    }
}

五、运行测试(直接执行 main 方法)

1. 生成密文(运行 Sm4Utils中main方法)

=== SM4 加解密测试 ===
加密前明文:123456
加密后密文:1bb11e4cf9eebd2538d53ebcaacb9cfe
解密后明文:123456

复制加密后的密文,直接填入 application.yml 的 ENC(xxx) 中。

2. 配置解密(运行 ApplicationConfigUtils工具类中main方法)

=== 配置文件解密测试 ===
单层配置 testPassword 解密结果:123456
多层配置 test.password 解密结果:123456

六、业务代码调用

无需注入、无需配置,可直接静态调用,即可获取解密后的真实值。如:

@SpringBootApplication
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
        // 获取配置信息
        String pwd2 = ApplicationConfigUtils.getProperty("test.password");
        System.out.println("pwd2: " + pwd2);
    }

}

七、问题解决

  1. 运行ApplicationConfigUtils中main方法报错,找不到配置文件
    原因:项目是多模块项目,项目有多个子模块(api,service,mapper,pojo,common),ApplicationConfigUtils工具类位置放到common子模块中,和application.yml配置文件不在同一模块。
  2. 报错:Unchecked cast: ‘java.lang.Object’ to ‘java.util.Map’
    原因:配置解析时类型转换不规范。解决方案:ApplicationConfigUtils工具类中getSafeMap方法中将 Object 安全强转为 Map<String, Object>,消除 Unchecked cast 警告。
  3. 报错:NullPointerException(空指针)
    原因1:application.yml文件不存在或路径错误 。解决方案:检查文件是否在resources目录下,文件名是否正确(application.yml)。
    原因2:SM4密钥未配置或配置错误。解决方案:检查yml中sm4.key是否配置,是否为16位。

八、总结

本文实现了一套轻量、无侵入、合规的 SpringBoot 国密 SM4 配置加密方案:

  1. 纯工具类实现,不依赖 Spring 容器,本地可直接测试;
  2. 自动识别 ENC(xxx) 格式,业务代码无感解密;
  3. 支持单层 / 多层 YML 配置,满足企业级项目需求;
  4. 完全符合等保、密评、国产化要求,可直接用于生产环境。

集成后彻底解决配置文件敏感信息明文泄露风险,一步到位满足安全合规要求。

更多推荐