jasypt springboot Error: Error creating bean with name ‘enableEncryptablePropertySourcesPostProcessor’ defined in class path resource

background

When using jasypt to encrypt sensitive information in the spring boot configuration file, an exception was encountered when using statuser to start directly

<dependency>
    <groupId>com.github.ulisesbocchio</groupId>
    <artifactId>jasypt-spring-boot-starter</artifactId>
    <version>3.0.3</version>
</dependency>

Encountered the following exception:

org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'enableEncryptablePropertySourcesPostProcessor' defined in class path resource [com/ulisesbocchio/jasyptspringboot/configuration/EnableEncryptablePropertiesConfiguration.class]: Unsatisfied dependency expressed through method 'enableEncryptablePropertySourcesPostProcessor' parameter 0; nested exception is org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'xxxDao' defined in xxxDao defined in @EnableJpaRepositories declared on Application: Unsatisfied dependency expressed through constructor parameter 1: Ambiguous argument values for parameter of type [javax.persistence.EntityManager] - did you specify the correct bean references as arguments?
	at org.springframework.beans.factory.support.ConstructorResolver.createArgumentArray(ConstructorResolver.java:797)
	at org.springframework.beans.factory.support.ConstructorResolver.instantiateUsingFactoryMethod(ConstructorResolver.java:538)
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.instantiateUsingFactoryMethod(AbstractAutowireCapableBeanFactory.java:1336)
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:1176)
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:556)
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:516)
	at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:324)
	at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234)
	at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:322)
	at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:207)
	at org.springframework.context.support.PostProcessorRegistrationDelegate.invokeBeanFactoryPostProcessors(PostProcessorRegistrationDelegate.java:172)
	at org.springframework.context.support.AbstractApplicationContext.invokeBeanFactoryPostProcessors(AbstractApplicationContext.java:707)
	at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:533)
	at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.refresh(ServletWebServerApplicationContext.java:143)
	at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:758)
	at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:750)
	at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:397)
	at org.springframework.boot.SpringApplication.run(SpringApplication.java:315)
	at org.springframework.boot.SpringApplication.run(SpringApplication.java:1237)
	at org.springframework.boot.SpringApplication.run(SpringApplication.java:1226)
...

The above information refers to the enableEncryptablePropertySourcesPostProcessorfailure due to the failure to create other types of Bean when it was created.

Let me talk about this problem first because the spring boot BeanFactoryPostProcessor and the custom repositoryFactoryBeanClass in the custom @EnableJpaRepositories are not compatible when they are created at startup. The @Repository annotated bean is initialized in advance (when the enableEncryptablePropertySourcesPostProcessor is created, because of the spring boot mechanism As a result, some classes are instantiated in advance, but the BeanFactoryPostProcessor processing @Repository has not been loaded)

If you do not customize the custom repositoryFactoryBeanClass in @EnableJpaRepositories, the above exception will not occur

Solution

After that, I wanted to implement the jasypt method by myself, but the BeanFactoryPostProcessor implementation method that still needs jasypt appeared, so I gave up, and finally used the rewriting PropertySource method, plus reflection to complete itself to decrypt the encrypted information that has been read (in the Before the bean is placed in the container, that is, before the annotations such as @Value take effect)

1. Introduce the following packages, remove the spring boot stater package of jasypt

<dependency>
    <groupId>com.github.ulisesbocchio</groupId>
    <artifactId>jasypt-spring-boot</artifactId>
    <version>3.0.3</version>
</dependency>

2. Define @Configuration to inject PropertySource beans

//JasyptPropertyValueConfig.java

import org.springframework.beans.factory.config.PropertyOverrideConfigurer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class JasyptPropertyValueConfig {

    @Bean
    public PropertyOverrideConfigurer jasyptPropertyOverrideConfigurer() {
        return new JasyptPropertyValueHandler();
    }
}

//JasyptPropertyValueHandler.java

import org.jasypt.util.text.BasicTextEncryptor;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.config.PropertyOverrideConfigurer;
import org.springframework.boot.env.OriginTrackedMapPropertySource;
import org.springframework.boot.origin.OriginTrackedValue;
import org.springframework.context.EnvironmentAware;
import org.springframework.core.env.Environment;
import org.springframework.core.env.MutablePropertySources;
import org.springframework.core.env.PropertySource;
import org.springframework.core.env.SimpleCommandLinePropertySource;
import org.springframework.web.context.support.StandardServletEnvironment;

import java.lang.reflect.Field;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.stream.StreamSupport;


public class JasyptPropertyValueHandler extends PropertyOverrideConfigurer implements EnvironmentAware {

    private static BasicTextEncryptor textEncryptor = null;
    private final String KEY_SEED = "jasypt.encryptor.password";
    private final String PREFIX = "ENC(";
    private final String SUFFIX = ")";
    private final byte[] tmp_lock = new byte[1];
    private boolean isInit;
    private String seed;
    private Environment environment;

    public JasyptPropertyValueHandler() {

    }

    @Override
    public void setEnvironment(Environment environment) {
        this.environment = environment;
    }

    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
        MutablePropertySources propertySources = ((StandardServletEnvironment) environment).getPropertySources();
        convertPropertySources(propertySources);
        super.postProcessBeanFactory(beanFactory);
    }


    public void convertPropertySources(MutablePropertySources propSources) {
        initSeed();
        // Command line parameter SimpleCommandLinePropertySource
        // yml profile parametersOriginTrackedMapPropertySource
        StreamSupport.stream(propSources.spliterator(), false)
                .filter(ps -> (ps instanceof OriginTrackedMapPropertySource) || (ps instanceof SimpleCommandLinePropertySource))
                .forEach(ps -> {
                    if (ps instanceof OriginTrackedMapPropertySource) {
                        handleConfigFile(ps);
                    } else if (ps instanceof SimpleCommandLinePropertySource) {
                        handleCommandLine(ps);
                    }
                    propSources.replace(ps.getName(), ps);
                });
    }
    //Handle spring boot's default configuration file, such as application.yml or application.properties to load all the content
    private void handleConfigFile(PropertySource ps) {
        Map<String, OriginTrackedValue> result = (Map<String, OriginTrackedValue>) ps.getSource();
        for (String key : result.keySet()) {
            OriginTrackedValue value = result.get(key);
            if (checkNeedProcessOverride(key, String.valueOf(value.getValue()))) {
                System.out.println(value);
                String decryptedValue = decryptValue(seed, String.valueOf(value.getValue()));
                try {
                    Field valueField = OriginTrackedValue.class.getDeclaredField("value");
                    valueField.setAccessible(true);
                    valueField.set(value, decryptedValue);
                } catch (NoSuchFieldException e) {
                    e.printStackTrace();
                } catch (IllegalAccessException e) {
                    e.printStackTrace();
                }
            }
        }
    }
    //Handle the replacement of spring boot arguments on the command line, for example in the form of the --spring.datasource.password argument
    private void handleCommandLine(PropertySource ps) {
        try {
            Object commandLineArgs = ps.getSource();
            Field valueField = commandLineArgs.getClass().getDeclaredField("optionArgs");
            valueField.setAccessible(true);
            boolean hasEncrypt = false;
            Map<String, List<String>> result = (Map<String, List<String>>) valueField.get(commandLineArgs);
            for (String key : result.keySet()) {
                List<String> values = result.get(key);
                if (values.size() == 1) {
                    if (checkNeedProcessOverride(key, String.valueOf(values.get(0)))) {
                        hasEncrypt = true;
                        String decryptedValue = decryptValue(seed, String.valueOf(values.get(0)));
                        values.clear();
                        values.add(decryptedValue);
                    }
                }
            }

            if (hasEncrypt) {
                valueField.set(commandLineArgs, result);
            }


        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }

    }


    private boolean checkNeedProcessOverride(String key, String value) {
        if (KEY_SEED.equals(key)) {
            return false;
        }
        return StringUtils.isNotBlank(value) && value.startsWith(PREFIX) && value.endsWith(SUFFIX);
    }

    private void initSeed() {
        if (!this.isInit) {
            this.isInit = true;
            this.seed = this.environment.getProperty(KEY_SEED);
            if (StringUtils.isNotBlank(this.seed)) {
                return;
            }
            try {
                Properties properties = mergeProperties();
                //From the startup command line, get the value of -Djasypt.encryptor.password
                this.seed = properties.getProperty(KEY_SEED);
            } catch (Exception e) {
                System.out.println("No encryption key configured");
            }
        }
    }

    private String decryptValue(String seed, String value) {
        value = value.replace(PREFIX, "").replace(SUFFIX, "");
        value = getEncryptor(seed).decrypt(value);
        return value;
    }

    private BasicTextEncryptor getEncryptor(String seed) {
        if (textEncryptor == null) {
            synchronized (tmp_lock) {
                if (textEncryptor == null) {
                    textEncryptor = new BasicTextEncryptor();
                    textEncryptor.setPassword(seed);
                }
            }
        }
        return textEncryptor;
    }
}

//JasyptController.java

import org.jasypt.util.text.BasicTextEncryptor;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("jasypt")
public class JasyptController {
    @Value(${jasypt.encryptor.password})
    private String seed;
    private static BasicTextEncryptor textEncryptor = null;
    private final byte[] tmp_lock = new byte[1];

    @RequestMapping("encrypt")
    public String encrypt(String value){
        textEncryptor=getEncryptor(seed);
        return textEncryptor.encrypt(value);
    }

    @RequestMapping("decrypt")
    public String decrypt(String value){
        textEncryptor=getEncryptor(seed);
        return textEncryptor.decrypt(value);
    }

    private BasicTextEncryptor getEncryptor(String seed) {
        if (textEncryptor == null) {
            synchronized (tmp_lock) {
                if (textEncryptor == null) {
                    textEncryptor = new BasicTextEncryptor();
                    textEncryptor.setPassword(seed);
                }
            }
        }
        return textEncryptor;
    }
}

  • After the project is started /jasypt/encrypt?value=Need to encrypt content, you can request the interface to get the ciphertext

how to use

After the above configuration is completed, you can use the configuration file and command line method of jasypt

1. In the configuration file

Write an application.properties here

spring.datasource.password=ENC(Encrypted ciphertext)

Then the command line uses the password to start the jar package

java -Djasypt.encrypt.password=Encrypted password -jar  xxx.jar

2. Command line

java -Djasypt.encrypt.password=Encrypted password -jar --spring.datasource.password=ENC(Encrypted ciphertext) xxx.jar

The above follows the coverage priority of spring boot.

to sum up

Because this is not an implementation of jasypt, it just simulates the commonly used configuration files and command decryption methods by default, so the custom content of jasypt cannot be used, and those who are interested can implement it by themselves

tips:  
    1. jasypt the same content is encrypted with different ciphertext every time  
    2. The ciphertext after different items encrypt the same content cannot be decrypted by the same password

Read More:

Leave a Reply

Your email address will not be published. Required fields are marked *