No Third‑Party Dependencies: Build a Custom Spring Boot Dynamic Decryption Component

The article explains how to protect database passwords and API keys in Spring Boot 3.5.0 by implementing a lightweight, third‑party‑free dynamic decryption component that encrypts values with AES, stores them with a {cipher} prefix, and decrypts them at pre‑refresh using a custom ApplicationContextInitializer.

Spring Full-Stack Practical Cases
Spring Full-Stack Practical Cases
Spring Full-Stack Practical Cases
No Third‑Party Dependencies: Build a Custom Spring Boot Dynamic Decryption Component

1. Introduction

In micro‑service architectures, storing database passwords, API keys and other sensitive configuration in plain text poses serious security and compliance risks. A common approach is to store them encrypted and decrypt at runtime. Although third‑party libraries such as Jasypt exist, the article demonstrates how to implement the same functionality using only Spring Boot core components, avoiding external supply‑chain dependencies.

2. Practical implementation

2.1 Encryption utility

public class SecretComonent {
    private static final String ALGORITHM = "AES";
    private static final String TRANSFORMATION = "AES/ECB/PKCS5Padding";

    public static String encrypt(String content, String key) {
        try {
            SecretKeySpec keySpec = new SecretKeySpec(key.getBytes(StandardCharsets.UTF_8), ALGORITHM);
            Cipher cipher = Cipher.getInstance(TRANSFORMATION);
            cipher.init(Cipher.ENCRYPT_MODE, keySpec);
            byte[] encrypted = cipher.doFinal(content.getBytes(StandardCharsets.UTF_8));
            return Base64.getEncoder().encodeToString(encrypted);
        } catch (Exception e) {
            throw new RuntimeException("AES 加密失败", e);
        }
    }

    public static String decrypt(String encryptedStr, String key) {
        try {
            SecretKeySpec keySpec = new SecretKeySpec(key.getBytes(StandardCharsets.UTF_8), ALGORITHM);
            Cipher cipher = Cipher.getInstance(TRANSFORMATION);
            cipher.init(Cipher.DECRYPT_MODE, keySpec);
            byte[] decoded = Base64.getDecoder().decode(encryptedStr);
            byte[] decrypted = cipher.doFinal(decoded);
            return new String(decrypted, StandardCharsets.UTF_8);
        } catch (Exception e) {
            throw new RuntimeException("AES 解密失败", e);
        }
    }
}

2.2 Container context initializer

public class DecryptApplicationContextInitializer implements ApplicationContextInitializer<ConfigurableApplicationContext> {
    private static final Logger logger = LoggerFactory.getLogger(DecryptApplicationContextInitializer.class);
    public static final String DECRYPTED_PROPERTY_SOURCE_NAME = "decrypted";
    public static final String ENCRYPTED_PROPERTY_PREFIX = "{cipher}";

    @Override
    public void initialize(ConfigurableApplicationContext applicationContext) {
        ConfigurableEnvironment environment = applicationContext.getEnvironment();
        MutablePropertySources propertySources = environment.getPropertySources();
        String key = environment.getProperty("pack.encrypt.key");
        Map<String, Object> map = decrypt(key, propertySources);
        if (!map.isEmpty()) {
            propertySources.addFirst(new SystemEnvironmentPropertySource(DECRYPTED_PROPERTY_SOURCE_NAME, map));
        }
    }

    protected Map<String, Object> decrypt(String key, PropertySources propertySources) {
        Map<String, Object> properties = merge(propertySources);
        decrypt(properties, key);
        return properties;
    }

    protected Map<String, Object> merge(PropertySources propertySources) {
        Map<String, Object> properties = new LinkedHashMap<>();
        List<PropertySource<?>> sources = new ArrayList<>();
        for (PropertySource<?> source : propertySources) {
            sources.add(0, source);
        }
        for (PropertySource<?> source : sources) {
            merge(source, properties);
        }
        return properties;
    }

    protected void merge(PropertySource<?> source, Map<String, Object> properties) {
        if (source instanceof CompositePropertySource) {
            List<PropertySource<?>> sources = new ArrayList<>(((CompositePropertySource) source).getPropertySources());
            Collections.reverse(sources);
            for (PropertySource<?> nested : sources) {
                merge(nested, properties);
            }
        } else if (source instanceof EnumerablePropertySource<?> enumerable) {
            for (String key : enumerable.getPropertyNames()) {
                Object property = source.getProperty(key);
                if (property != null) {
                    String value = property.toString();
                    if (value.startsWith(ENCRYPTED_PROPERTY_PREFIX)) {
                        properties.put(key, value);
                    }
                }
            }
        }
    }

    protected void decrypt(Map<String, Object> properties, String secretKey) {
        properties.replaceAll((k, v) -> {
            String valueString = v.toString();
            if (!valueString.startsWith(ENCRYPTED_PROPERTY_PREFIX)) {
                return v;
            }
            return decrypt(k, valueString, secretKey);
        });
    }

    protected String decrypt(String key, String original, String secretKey) {
        String value = original.substring(ENCRYPTED_PROPERTY_PREFIX.length());
        try {
            value = SecretComonent.decrypt(value, secretKey);
            return value;
        } catch (Exception e) {
            logger.warn("Cannot decrypt: key=", key, e);
            return "";
        }
    }
}

2.3 Configuration and testing

Add the initializer to META-INF/spring.factories:

org.springframework.context.ApplicationContextInitializer=\
com.pack.encrypt.initializer.DecryptApplicationContextInitializer

Encrypt the password in application.yml using the {cipher} prefix:

common-datasource: &common-ds
  driver-class-name: com.mysql.cj.jdbc.Driver
  username: root
  password: '{cipher}BlgIeTDSKwIfwjdXk5TzKQ=='
  type: com.zaxxer.hikari.HikariDataSource
spring:
  datasource:
    url: jdbc:mysql://localhost:3306/testjpa?serverTimezone=GMT%2B8&useSSL=false&characterEncoding=UTF-8&rewriteBatchedStatements=true&cachePrepStmts=true
    <<: *common-ds

Start the application with the encryption key passed as a JVM argument: --pack.encrypt.key=aaaabbbbccccdddd If the initializer is correctly registered, the application starts successfully (see screenshot). Without it, startup fails with an error indicating that the encrypted property cannot be resolved (see screenshot).

Successful startup screenshot
Successful startup screenshot
Error screenshot when initializer missing
Error screenshot when initializer missing

By leveraging Spring Boot’s pre‑refresh hook and the standard Java cryptography API, the solution provides a lightweight, fully self‑contained way to keep sensitive configuration encrypted at rest while still being usable at runtime.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

Spring BootAESapplicationcontextinitializerConfiguration SecurityDynamic Decryption
Spring Full-Stack Practical Cases
Written by

Spring Full-Stack Practical Cases

Full-stack Java development with Vue 2/3 front-end suite; hands-on examples and source code analysis for Spring, Spring Boot 2/3, and Spring Cloud.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.