Spring Boot 中使用 SM2 解密 Nacos 配置文件

338 阅读5分钟

在现代软件开发中,保护敏感配置数据至关重要。本文介绍如何在Spring Boot中使用SM2算法解密Nacos配置文件,并初始化应用程序上下文。

什么是SM2算法?

SM2算法是中国国家密码管理局制定的椭圆曲线公钥密码算法,具有高安全性和高效率的特点,广泛应用于数据加密和数字签名。

实现解密逻辑

我们将通过实现一个ApplicationContextInitializer来在Spring Boot应用程序初始化时解密Nacos配置文件。

@Slf4j
public class SM2DecryptApplicationInitializer implements ApplicationContextInitializer<ConfigurableApplicationContext> {

    public SM2DecryptApplicationInitializer() {}

    @Override
    public void initialize(ConfigurableApplicationContext applicationContext) {
        ConfigurableEnvironment environment = applicationContext.getEnvironment();
        MutablePropertySources propertySources = environment.getPropertySources();
        List<PropertySource<?>> propertySourcesToDecrypt = new ArrayList<>();

        for (PropertySource<?> propertySource : propertySources) {
            collectPropertySources(propertySource, propertySourcesToDecrypt);
        }

        for (PropertySource<?> propertySource : propertySourcesToDecrypt) {
            decryptAndReplacePropertySource(propertySource, propertySources);
        }
    }

    private void collectPropertySources(PropertySource<?> propertySource, List<PropertySource<?>> propertySourcesToDecrypt) {
        if (propertySource instanceof CompositePropertySource) {
            CompositePropertySource composite = (CompositePropertySource) propertySource;
            for (PropertySource<?> source : composite.getPropertySources()) {
                collectPropertySources(source, propertySourcesToDecrypt);
            }
        } else if (propertySource instanceof NacosPropertySource) {
            propertySourcesToDecrypt.add(propertySource);
        }
    }

    private void decryptAndReplacePropertySource(PropertySource<?> propertySource, MutablePropertySources propertySources) {
        if (propertySource instanceof NacosPropertySource) {
            NacosPropertySource nacosPropertySource = (NacosPropertySource) propertySource;
            String fileExtension = determineFileExtension(nacosPropertySource.getName());
            Map<String, Object> properties = nacosPropertySource.getSource();
            Map<String, Object> decryptedProperties = new HashMap<>();

            for (Map.Entry<String, Object> entry : properties.entrySet()) {
                String key = entry.getKey();
                Object value = entry.getValue();
                if (value instanceof String) {
                    try {
                        String decryptedValue = SMUtils.sm2Decrypt((String) value, System.getProperty("privateKey"));
                        decryptedProperties = propertiesToMap(loadNacosData(decryptedValue, fileExtension));
                    } catch (Exception e) {
                        throw new RuntimeException("SM2解密时出现异常:" + e.getMessage());
                    }
                    log.info("Decrypted property: {} = {}", key, decryptedValue);
                }
            }

            try {
                Constructor<NacosPropertySource> constructor = NacosPropertySource.class.getDeclaredConstructor(
                        String.class, String.class, Map.class, Date.class, boolean.class);

                constructor.setAccessible(true);

                NacosPropertySource decryptedNacosPropertySource = constructor.newInstance(
                        nacosPropertySource.getGroup(),
                        nacosPropertySource.getDataId(),
                        decryptedProperties,
                        nacosPropertySource.getTimestamp(),
                        nacosPropertySource.isRefreshable());

                boolean replaced = replacePropertySource(propertySources, nacosPropertySource.getName(), decryptedNacosPropertySource);

                if (replaced) {
                    log.info("Replaced NacosPropertySource: {}", nacosPropertySource.getName());
                } else {
                    log.info("Failed to replace NacosPropertySource: {}", nacosPropertySource.getName());
                }
            } catch (Exception e) {
                throw new RuntimeException("Failed to create NacosPropertySource using reflection", e);
            }
        }
    }

    private String determineFileExtension(String name) {
        if (name.contains("yml")) {
            return "yml";
        } else if (name.contains("yaml")) {
            return "yaml";
        } else {
            return "properties";
        }
    }

    private boolean replacePropertySource(MutablePropertySources propertySources, String name, PropertySource<?> newPropertySource) {
        log.info("Replacing PropertySource: {}", name);
        List<PropertySource<?>> propertySourcesList = new ArrayList<>();
        propertySources.forEach(propertySourcesList::add);

        for (int i = 0; i < propertySourcesList.size(); i++) {
            PropertySource<?> propertySource = propertySourcesList.get(i);
            if (propertySource.getName().equals(name)) {
                propertySources.replace(name, newPropertySource);
                return true;
            } else if (propertySource instanceof CompositePropertySource) {
                CompositePropertySource compositePropertySource = (CompositePropertySource) propertySource;
                if (replaceInCompositePropertySource(compositePropertySource, name, newPropertySource)) {
                    return true;
                }
            }
        }
        return false;
    }

    private boolean replaceInCompositePropertySource(CompositePropertySource compositePropertySource, String name, PropertySource<?> newPropertySource) {
        List<PropertySource<?>> tempList = new ArrayList<>(compositePropertySource.getPropertySources());

        boolean replaced = false;
        for (int i = 0; i < tempList.size(); i++) {
            PropertySource<?> propertySource = tempList.get(i);
            if (propertySource.getName().equals(name)) {
                tempList.set(i, newPropertySource);
                replaced = true;
                break;
            } else if (propertySource instanceof CompositePropertySource) {
                if (replaceInCompositePropertySource((CompositePropertySource) propertySource, name, newPropertySource)) {
                    replaced = true;
                    break;
                }
            }
        }

        if (replaced) {
            compositePropertySource.getPropertySources().clear();
            for (PropertySource<?> propertySource : tempList) {
                compositePropertySource.addPropertySource(propertySource);
            }
        }

        return replaced;
    }

    private Map<String, Object> propertiesToMap(Properties properties) {
        Map<String, Object> result = new HashMap<>(16);
        Enumeration<String> keys = (Enumeration<String>) properties.propertyNames();
        while (keys.hasMoreElements()) {
            String key = keys.nextElement();
            Object value = properties.getProperty(key);
            if (value != null) {
                result.put(key, ((String) value).trim());
            } else {
                result.put(key, null);
            }
        }
        return result;
    }

    private Properties loadNacosData(String data, String fileExtension) throws IOException {
        if (fileExtension.equalsIgnoreCase("properties")) {
            Properties properties = new Properties();
            properties.load(new StringReader(data));
            return properties;
        } else if (fileExtension.equalsIgnoreCase("yaml") || fileExtension.equalsIgnoreCase("yml")) {
            YamlPropertiesFactoryBean yamlFactory = new YamlPropertiesFactoryBean();
            yamlFac


代码解析

  1. 初始化方法initialize方法在Spring Boot应用上下文初始化时调用,负责获取Spring环境并遍历所有属性源,收集需要解密的部分。

  2. 收集需要解密的属性源collectPropertySources方法递归遍历属性源,收集需要解密的NacosPropertySource

  3. 解密并替换属性源decryptAndReplacePropertySource方法对收集到的属性源进行解密,并使用反射创建新的NacosPropertySource实例,替换原有的属性源。

  4. 确定文件扩展名determineFileExtension方法根据属性源名称确定文件的扩展名(ymlyamlproperties)。

  5. 替换属性源replacePropertySourcereplaceInCompositePropertySource方法负责在属性源集合中替换旧的属性源为解密后的新属性源。

  6. Properties转MappropertiesToMap方法将Properties对象转换为Map,便于后续操作。

  7. 加载配置文件数据loadNacosData方法根据文件扩展名加载配置文件数据,并返回Properties对象。

Spring Boot初始化流程

ApplicationContextInitializer接口允许在Spring应用上下文刷新之前对其进行自定义初始化。这在以下场景中非常有用:

  • 配置解密:如本文示例,解密敏感配置数据。

  • 动态配置加载:根据环境动态调整配置源。

  • 预加载资源:在上下文刷新之前加载必要的资源。

public class SM2EnvironmentDecryptApplicationInitializer implements ApplicationContextInitializer<ConfigurableApplicationContext> {
    @Override
    public void initialize(ConfigurableApplicationContext applicationContext) {
        // 自定义初始化逻辑
    }
}

MutablePropertySources的使用

MutablePropertySources用于管理应用程序中的属性源。通过将解密后的属性源替换到MutablePropertySources中,确保了应用程序在运行时使用的配置是解密后的。

Nacos配置管理角度

Nacos的引入

Nacos是一款动态服务发现、配置管理和服务管理平台。它提供了对配置文件的集中管理和动态更新能力。本示例展示了如何在Spring Boot应用中解密Nacos配置,并将其加载到应用环境中。

CompositePropertySource的处理

CompositePropertySource允许将多个属性源组合在一起。递归处理CompositePropertySource确保了嵌套属性源也能被正确识别和解密。

if (propertySource instanceof CompositePropertySource) {
    CompositePropertySource composite = (CompositePropertySource) propertySource;
    for (PropertySource<?> source : composite.getPropertySources()) {
        collectPropertySources(source, propertySourcesToDecrypt);
    }
}

反射的应用

反射机制的使用

在Java中,反射允许在运行时动态访问和操作类和对象的成员。在本示例中,反射用于创建新的NacosPropertySource实例。

Constructor<NacosPropertySource> constructor = NacosPropertySource.class.getDeclaredConstructor(
        String.classString.class, Map.classDate.classboolean.class);
constructor.setAccessible(true);
NacosPropertySource decryptedNacosPropertySource = constructor.newInstance(
        nacosPropertySource.getGroup(),
        nacosPropertySource.getDataId(),
        decryptedProperties,
        nacosPropertySource.getTimestamp(),
        nacosPropertySource.isRefreshable());

原理图

+-------------------------+
| Application Entry Point |
|     (main method)       |
+-----------+-------------+
            |
            v
+-----------v-------------+
|    SpringApplication    |
|  (run method is called|
+-----------+-------------+
            |
            v
+-----------v-------------+
|  Prepare Environment    |
| - Read and load properties |
| - Setup profiles        |
+-----------+-------------+
            |
            v
+-----------v-------------+
| Setup ApplicationContext|
| - Create IOC container  |
| - Register beans        |
+-----------+-------------+
            |
            v
+-----------v-------------+
|   Application Context   |
|      Initializers       |
|  (initialize method is  |
|        called)          |
|    +----------------+   |
|    | SM2 Decrypt    |   |
|    | Properties     |   |
|    +----------------+   |
+-----------+-------------+
            |
            v
+-----------v-------------+
|  Load Bean Definitions  |
| - Scan for @Component,  |
|   @Service@Repository,|
|   @Controller           |
+-----------+-------------+
            |
            v
+-----------v-------------+
|      Bean Creation      |
| - Create beans and      |
|   inject dependencies   |
+-----------+-------------+
            |
            v
+-----------v-------------+
|     ApplicationRunner   |
|    CommandLineRunner    |
|      (run method)       |
+-----------+-------------+
            |
            v
+-----------v-------------+
|    Application Ready    |
| - Application is now    |
|   ready to serve requests|
+-------------------------+


总结

通过实现ApplicationContextInitializer并使用SM2算法对Nacos配置文件进行解密,我们能够在Spring Boot应用程序启动时确保敏感数据的安全。上述代码展示了如何收集、解密和替换属性源,使得解密过程无缝集成到应用程序的初始化过程中。

这种方法不仅提高了配置管理的安全性,还保持了Spring Boot应用程序的灵活性和可维护性。希望本文的介绍能够帮助您在实际项目中更好地实现配置文件的加密和解密管理。