SoFunction
Updated on 2025-03-04

Common way to change the password plaintext to ciphertext processing in Spring configuration files

1. Background

SpringBoot and SpringCloud involve multiple configuration files. The default password for the configuration files is in plaintext. This method is generally not allowed in production environments. To avoid plaintext in the configuration file, it should be configured as ciphertext in the configuration file, and then decryption is completed inside the program at startup.

This article provides a general processing method, which can be adapted to the following types of configuration files:

  • localConfiguration before Spring's bean creation
  • localConfiguration in Spring, including configuration with profile environment
  • Configuration on the configuration center (such as Data ID on Nacos)   

In order to adapt to the configuration file to change the password from plain text to cipher text, it needs to be divided into two steps:

① Configure the configuration items involved in the ciphertext in the configuration file as ciphertext strings (need to encrypt and calculate by yourself);

② Read the ciphertext string in Spring startup and decrypt and restore.

2. Ideas

For the processing when Spring starts in step 2 above, since the above configuration file is different in Spring loading timing and life cycle, there are two ways to deal with it:

A) Normal way

Since Spring's configurations on local or configuration center (such as Data ID on Nacos) will have corresponding configuration beans (by annotating the Java class declared by @Configuration), Spring will automatically parse the configuration file according to the read and assign it to the bean.

Therefore, if you need to decrypt and restore the ciphertext string, you can inherit the configuration bean (by annotating the Java class declared by @Configuration), and Override rewrites the corresponding set method to complete the decryption.

B) Suitable method

For Spring Cloud, the bean has not been created in the bootstrap stage, so the above Override rewrite corresponding set method does not apply. So for configuration files. The Environment configuration can be captured by implementing the EnvironmentPostProcessor interface, and after decryption, the configuration new value is set to the Environment.

III. Example

A) Normal method (connect to Redis cluster)

The following is an example of connecting to the Redis cluster. The configuration item connected to the Redis cluster can be configured locally or on the configuration center (such as Data ID on Nacos), and the configuration item value has been set to the ciphertext.

The following code inherits the configuration bean (by annotating the Java class RedisProperties declared by @Configuration), and Override rewrites the corresponding set method. The Java code is as follows:

package Package specifications are ignored,Please make your own;
 
import Ignore the decryption computing tool classSystemSecurityAlgorithm,Please make your own;
import ;
import ;
import ;
import ;
 
/**
  *Configuration class for connecting to Redis cluster [overwrite the original bean mechanism through @Configuration]:
  * 1. The connection password connected to Redis must not appear plain text, so it needs to be configured as an encrypted ciphertext in the properties configuration file (the encryption algorithm Java class is: SystemSecurityAlgorithm), and then decrypt it through this class when starting.
  * 2. Precious metal application services are deployed using multi-data center DataCenter.  Each logical center has an independent Redis cluster.  Application services should be connected to Redis clusters in the same logical center. Application services in Beijing should not be connected to Redis clusters in Hefei.
  *: For different instances of the same service, the Redis cluster under the same logical center should be connected according to the logical center where the service instance is located (see the logical center defined in the enumeration for details).
  *     therefore:
  * a). Based on the Spring standard Redis connection configuration, for each IP port configuration in the nodes value, add a capital letter before each IP: the English code of the DataCenter data center where the IP is located
  * b). Based on the Spring standard Redis connection configuration, the password value can be changed to multiple passwords, separated by commas, and a capital letter is added before each password. The password is the English code of the DataCenter data center of which Redis cluster is connected to.
  * To support the above, this class is customized to implement the configuration of processing the final restore to the Spring standard connection Redis, so that lettuce can create a connection pool.
  *  -----------------------------------------------------------
  * Applicability of the mechanism:
  * In addition to overwriting the original Bean mechanism through @Configuration, the EnvironmentPostProcessor interface mechanism is also implemented.  The applicability of the two mechanisms is as follows:
  * Configuration file (bootstrap stage, bean has not been created yet) →→fit →→ 【Implement EnvironmentPostProcessor interface mechanism】
  * Local configuration file (normal SpringBoot startup, bean annotated through @Configuration) →→ suitable →→ [Implement EnvironmentPostProcessor interface mechanism] and [cover the original bean mechanism through @Configuration] are both OK
  * Obtain the configuration file obtained from Nacos and other configuration centers →→ suitable →→ [Overwrite the original bean mechanism through @Configuration]
  *
  */
@Configuration
@Primary // Since the default RedisProperties as the configuration class will automatically create beans.  In order to avoid the existence of two similar types (RedisProperties) beans, this class annotates Primary so that only this class takes effect.  Equivalent to replace the default RedisPropertiespublic class GjsRedisProperties extends RedisProperties {
 
    private static final org. log = org.();
 
    @Override
    public void setPassword(String orginPassword) {
        if((orginPassword)) {
            // Decrypt and set the ciphertext            if ((orginPassword) && () >= 32 ) { // If the length and upper case requirements of the password ciphertext are met, it will be considered as ciphertext and decrypted                String padStr = (orginPassword);
                ("connectRedisConfiguration Items: Before decryptionorginPassword=[{}], After decryptionpadStr=[{}]", orginPassword, padStr); //To avoid password leakage, only debug outputs plain text                ("connectRedisConfiguration Items: For cipher textorginPassword=[{}]Decryption completed", orginPassword);
                (padStr);
            } else { // Does not meet the length and case requirements of the password ciphertext (deemed as plain text, not decrypted), and remains unchanged                ("connectRedisConfiguration Items的:orginPassword=[{}]Does not meet the length and case requirements of the password ciphertext(Deemed as plain text,Not decrypted),Stay unchanged", orginPassword);
                (orginPassword);
            }
        }
    }
}

A) Normal method (connect to RocketMQ)

The following is an example of connecting to RocketMQ. The configuration items connecting to RocketMQ can be configured locally or on the configuration center (such as Data ID on Nacos), and the -key and -key configuration items values ​​have been set to ciphertext.

The following code inherits the configuration bean (by annotating the Java class RocketMQProperties declared by @Configuration), and Override rewrites the corresponding set method. The Java code is as follows:

package Package specifications are ignored,Please make your own;
 
import Ignore the decryption computing tool classSystemSecurityAlgorithm,Please make your own;
import ;
import ;
import ;
import ;
import ;
import ;
import ;
import ;
 
import ;
import ;
 
/**
  *Contact class for connecting RocketMQ [overwrite the original bean mechanism through @Configuration]:
  * Because the secret-key connected to RocketMQ must not have plain text, it must be configured as an encrypted ciphertext in the properties configuration file (the encryption algorithm Java class is: SystemSecurityAlgorithm), and then decrypt it through this class at startup.
  *  -----------------------------------------------------------
  * Applicability of the mechanism:
  * In addition to overwriting the original Bean mechanism through @Configuration, the EnvironmentPostProcessor interface mechanism is also implemented.  The applicability of the two mechanisms is as follows:
  * Configuration file (bootstrap stage, bean has not been created yet) →→fit →→ 【Implement EnvironmentPostProcessor interface mechanism】
  * Local configuration file (normal SpringBoot startup, bean annotated through @Configuration) →→ suitable →→ [Implement EnvironmentPostProcessor interface mechanism] and [cover the original bean mechanism through @Configuration] are both OK
  * Obtain the configuration file obtained from Nacos and other configuration centers →→ suitable →→ [Overwrite the original bean mechanism through @Configuration]
  *
  */
@Configuration
@Primary // Since the default RocketMQProperties as the configuration class will automatically create beans.  To avoid the existence of two same type (RocketMQProperties) beans, this class annotates Primary so that only this class takes effect.  Equivalent to replace the default RocketMQPropertiespublic class GjsRocketMQProperties extends RocketMQProperties {
 
    final private String KEYNAME_PRODUCER_SECRET = "-key";
    final private String KEYNAME_CONSUMER_SECRET = "-key";
 
    @Autowired
    ConfigurableApplicationContext springContext;
 
    private static final org. log = org.();
 
    @Override
    public void setProducer(Producer producer) {
        final String orginSecretKey = ();
        // Decrypt and set the ciphertext        if ((orginSecretKey) && () >= 32) { // If the length and upper case requirements of the password ciphertext are met, it will be considered as ciphertext and decrypted            String padStr = (orginSecretKey);
            ("connectRocketMQConfiguration Items{}: Before decryptionorginSecretKey=[{}], After decryptionpadStr=[{}]", KEYNAME_PRODUCER_SECRET, orginSecretKey, padStr); //To avoid password leakage, only debug outputs plain text            ("connectRocketMQConfiguration Items{}: For cipher textorginSecretKey=[{}]Decryption completed", KEYNAME_PRODUCER_SECRET, orginSecretKey);
            (padStr);
 
            // Because RocketMQ will get configuration from Spring's Environment during the construction of the DefaultRocketMQListenerContainer.            // A brief description of the call relationship is as follows:            //     ()
            //       ()
            //         ()
            //           ()
            //             ......
            //               ()
            // Therefore, modify the value in the environment together to obtain new values            modifyEnvironmentValue((), KEYNAME_PRODUCER_SECRET, padStr);
 
        } else { // Does not meet the length and case requirements of the password ciphertext (deemed as plain text, not decrypted), and remains unchanged            ("connectRocketMQConfiguration Items-keyvalue=[{}]Does not meet the length and case requirements of the password ciphertext(Deemed as plain text,Not decrypted),Stay unchanged", orginSecretKey);
        }
 
        (producer);
    }
 
    @Override
    public void setConsumer(PushConsumer pushConsumer) {
        final String orginSecretKey = ();
        // Decrypt and set the ciphertext        if ((orginSecretKey) && () >= 32 ) { // If the length and upper case requirements of the password ciphertext are met, it will be considered as ciphertext and decrypted            String padStr = (orginSecretKey);
            ("connectRocketMQConfiguration Items{}: Before decryptionorginSecretKey=[{}], After decryptionpadStr=[{}]", KEYNAME_CONSUMER_SECRET, orginSecretKey, padStr); //To avoid password leakage, only debug outputs plain text            ("connectRocketMQConfiguration Items{}: For cipher textorginSecretKey=[{}]Decryption completed", KEYNAME_CONSUMER_SECRET, orginSecretKey);
            (padStr);
 
            // Because RocketMQ will get configuration from Spring's Environment during the construction of the DefaultRocketMQListenerContainer.            // A brief description of the call relationship is as follows:            //     ()
            //       ()
            //         ()
            //           ()
            //             ......
            //               ()
            // Therefore, modify the value in the environment together to obtain new values            modifyEnvironmentValue((), KEYNAME_CONSUMER_SECRET, padStr);
 
        } else { // Does not meet the length and case requirements of the password ciphertext (deemed as plain text, not decrypted), and remains unchanged            ("connectRocketMQConfiguration Items{}ofvalue=[{}]Does not meet the length and case requirements of the password ciphertext(Deemed as plain text,Not decrypted),Stay unchanged", KEYNAME_CONSUMER_SECRET, orginSecretKey);
        }
 
        (pushConsumer);
    }
 
    @Override
    public void setPullConsumer(PullConsumer pullConsumer) {
        final String orginSecretKey = ();
        // Decrypt and set the ciphertext        if ((orginSecretKey) && () >= 32 ) { // If the length and upper case requirements of the password ciphertext are met, it will be considered as ciphertext and decrypted            String padStr = (orginSecretKey);
            ("connectRocketMQConfiguration Items{}: Before decryptionorginSecretKey=[{}], After decryptionpadStr=[{}]", KEYNAME_CONSUMER_SECRET, orginSecretKey, padStr); //To avoid password leakage, only debug outputs plain text            ("connectRocketMQConfiguration Items{}: For cipher textorginSecretKey=[{}]Decryption completed", KEYNAME_CONSUMER_SECRET, orginSecretKey);
            (padStr);
 
            // Because RocketMQ will get configuration from Spring's Environment during the construction of the DefaultRocketMQListenerContainer.            // A brief description of the call relationship is as follows:            //     ()
            //       ()
            //         ()
            //           ()
            //             ......
            //               ()
            // Therefore, modify the value in the environment together to obtain new values            modifyEnvironmentValue((), KEYNAME_CONSUMER_SECRET, padStr);
 
        } else { // Does not meet the length and case requirements of the password ciphertext (deemed as plain text, not decrypted), and remains unchanged            ("connectRocketMQConfiguration Items{}ofvalue=[{}]Does not meet the length and case requirements of the password ciphertext(Deemed as plain text,Not decrypted),Stay unchanged", KEYNAME_CONSUMER_SECRET, orginSecretKey);
        }
 
        (pullConsumer);
    }
 
 
    /**
      * Change the value of Spring Environment configuration item to a new value
      * @param environment Spring Environment object
      * @param keyName Configuration item name
      * @param newValue new value
      */
    private void modifyEnvironmentValue(ConfigurableEnvironment environment, final String keyName, String newValue) {
        if(!(keyName)) {
            ("currentSpringofenvironmentThe name does not exist in{}ofConfiguration Items", keyName);
            return;
        }
        if((keyName, "").equals(newValue)) {
            ("currentSpringofenvironment中Configuration Items{}ofvalue已与新value相同,No modification required", keyName);
            return;
        }
        Map<String, Object> map = new HashMap<>(); // Used to store new values        (keyName, newValue);
        // If there is a map with a value, add the map as PropertySource to the list to achieve: overwrite the value of the corresponding key in the environment as a new value        // Must be added to First and there cannot be two MapPropertySources with the same Name, so that the value overwrite can take effect        ().addFirst(new MapPropertySource("modifyEnvironmentValue-"+keyName, map));
        ("Already correctSpringofEnvironmentofConfiguration Items{}ofvalue修改为新value", keyName);
    }
}

B) Suitable method

The following is an example of connecting to the Nacos Configuration Center. You need to specify the Nacos username, password, server address, Data ID and other information for connecting to the Nacos Configuration Center in the local configuration file. The configuration file for connecting to the Nacos configuration center is similar to the following:

#authenticate authentication username and password of the Nacos configuration center and registration center (authentication is required on the Nacos server)=nacos
=760dee29f9fc82af0cc1d6074879dc39
#Nacos configure the address and port of the central server (form ip:port,ip:port,...).  Note: The addresses will be selected in order for connection (the next one will be automatically selected if the previous connection fails).  The address will be selected randomly for connection (if the connection fails, it will be automatically selected)-addr=ip1:8848,ip2:8848,ip3:8848,ip4:8848
 
#Data ID prefix (if not set, ${} is selected by default)#=
#Designed as development environment by default#=
#Nacos namespace, not set here, keep the default#=
#Configure group (if not set, the default is DEFAULT_GROUP)=G_CONFIG_GJS_SERVICE
#Specify file suffix (if not set, the default is properties)-extension=properties
 
#The following is the global Data ID-configs[0].data-id=
-configs[0].group=G_CONFIG_GJS_GLOBALSHARED
-configs[0].refresh=true
 
-configs[1].data-id=
-configs[1].group=G_CONFIG_GJS_GLOBALSHARED
-configs[1].refresh=true
 
-configs[2].data-id=
-configs[2].group=G_CONFIG_GJS_GLOBALSHARED
-configs[2].refresh=true

inThe configuration item value has been set to ciphertext.

The following code captures the configuration by implementing the EnvironmentPostProcessor interface and sets the new configuration value into the Environment. The Java code is as follows:

package Package specifications are ignored,Please make your own;
 
import Ignore the decryption computing tool classSystemSecurityAlgorithm,Please make your own;
import ;
import ;
import ;
import ;
import ;
import ;
import ;
import ;
import ;
 
import ;
import ;
 
/**
  * This class implements the EnvironmentPostProcessor interface to read the specified key value from the environment during Spring startup, and then overwrites the value of the corresponding key in the environment as the new value.
  * This class has implemented configuration file processing for bootstrap stage:
  * Because the password connected to Nacos must not have plain text, the bootstrap configuration file is an encrypted ciphertext (the encryption algorithm Java class is: SystemSecurityAlgorithm), and then decrypts it through this class at startup.
  * -----------------------------------------------------------
  * Notice:
  * a) This class needs to be configured in the file under META-INF before it will take effect (it will be recognized by Spring scan)
  * b) Because this class implements the EnvironmentPostProcessor interface method, this class will be called twice during SpringCloud startup:
  * First, after the bootstrap configuration file is loaded (SpringCloud is the bootstrap stage of the support configuration center)
  * Secondly, after the application configuration file is loaded (the configuration file stage is loaded during normal startup of SpringBoot)
  * Applicability of the mechanism:
  * In addition to implementing the EnvironmentPostProcessor interface mechanism, the original Bean mechanism is also overwritten through @Configuration.  The applicability of the two mechanisms is as follows:
  * Configuration file (bootstrap stage, bean has not been created yet) →→fit →→ 【Implement EnvironmentPostProcessor interface mechanism】
  * Local configuration file (normal SpringBoot startup, bean annotated through @Configuration) →→ suitable →→ [Implement EnvironmentPostProcessor interface mechanism] and [cover the original bean mechanism through @Configuration] are both OK
  * Obtain the configuration file obtained from Nacos and other configuration centers →→ suitable →→ [Overwrite the original bean mechanism through @Configuration]
  *
  */
public class GjsEnvironmentPostProcessor implements EnvironmentPostProcessor, Ordered {
 
    /**
      * The default order for the processor. The smaller the value, the higher the priority
      * Because the bootstrap configuration file is loaded through {@link}
      * Since this EnvironmentPostProcessor class needs to wait for SpringCloud to bootstrap configuration file before it can be executed, the EnvironmentPostProcessor class must be given a lower priority.
      */
    public static final int ORDER = Ordered.HIGHEST_PRECEDENCE + 50;
 
    private final DeferredLogFactory logFactory;
 
    private final Log logger;
 
    public GjsEnvironmentPostProcessor(DeferredLogFactory logFactory,
                                       ConfigurableBootstrapContext bootstrapContext) {
         = logFactory;
         = (getClass());
    }
 
 
    @Override
    public int getOrder() {
        return ORDER;
    }
 
 
    /**
      * Read the specified key from the environment and decrypt it. Put the decrypted result into the map object
      * @param environment Already Spring environment
      * @param keyName The specified key name
      * @param map If decryption is completed, the decrypted result will be placed in the map object
      */
    private void decodePwd(ConfigurableEnvironment environment, String keyName, Map<String, Object> map ) {
        if(!(keyName)) {
            ("EnvironmentPostProcessor The current Spring environment does not exist as "+keyName+"Configuration Items");
            return;
        }
 
        final String origalValue = (keyName);
        // Decrypt and set the ciphertext        if ((origalValue) && () >= 32) { // If the length and upper case requirements of the password ciphertext are met, it will be considered as ciphertext and decrypted            String padStr = (origalValue);
            ("EnvironmentPostProcessor Configuration Item"+keyName+"Original value=["+origalValue+"], Decrypted value=["+padStr+"]"); //To avoid password leakage in the log, only debug outputs plain text            ("EnvironmentPostProcessor Configuration Item"+keyName+"Original value=["+origalValue+"]Decryption completed");
            (keyName, padStr);
        }else {
            ("EnvironmentPostProcessor Configuration Item"+keyName+"value=["+origalValue+"]Does not meet the length and case requirements of the password ciphertext(Deemed as plain text,Not decrypted),Stay unchanged");
        }
    }
 
    @Override
    public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) {
        ("EnvironmentPostProcessor before PropertySources size=" + ().size());
        ("EnvironmentPostProcessor before PropertySources : " + ());
        Map<String, Object> map = new HashMap<>(); // Used to store new values 
        decodePwd(environment, "", map);
 
        if(!()) {
            // If there is a map with a value, add the map as PropertySource to the list to achieve: overwrite the value of the corresponding key in the environment as a new value            // Must be added to First and there cannot be two MapPropertySources with the same Name, so that the value overwrite can take effect            ().addFirst(new MapPropertySource("afterDecodePassword", map));
        }
        ("EnvironmentPostProcessor after PropertySources size=" + ().size());
        ("EnvironmentPostProcessor after PropertySources : " + ());
    }
 
}

4. Summary

Through the above two methods, the adaptation and processing of configuration ciphertexts by various Spring configuration files can be solved.

At the same time, it is not only used for ciphertexts, but any changes to the content of the configuration file can be processed in the above manner. For example, when starting, dynamically use multiple IPs in the configuration item value, etc.

The above is the detailed content of the general way to process the password plaintext in Spring configuration files. For more information about changing Spring password plaintext to ciphertext, please pay attention to my other related articles!