Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

SpEL refactoring in @Value (encrypted property)

I have a Spring Security configuration where I have to use encrypted properties. I have utility static method PasswordUtil.decode() by which I can decode a property for the further usage.

Following solution works, but the particular SpEL looks quite ugly to me. So, my question: Is it possible to refactor given SpEL expression to something nicer/shorter/idiomatic?

@Configuration
@EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {

    @Value("#{T(my.package.PasswordUtil).decode('${signkey.password}')}")
    private String signKeyPassword;

}
like image 462
Vít Kotačka Avatar asked Jan 27 '26 04:01

Vít Kotačka


2 Answers

You can register a custom SpEL Resolver Function as you can see in the Reference Documentation. What that means is that you can create a custom SpEL keyword that would resolve in your custom code underneath also supporting inputs.

In other words you'll be able to write instead of:

@Value("#{T(my.package.PasswordUtil).decode('${signkey.password}')}")
private String signKeyPassword;

the below where your custom SpEL keyword is mydecode:

@Value("#{ #mydecode( '${signkey.password}' ) }")
String signKeyPassword;

This option: 1) significantly reduces the SpEL string literal, 2) gives you the chance to pick up a name that makes sense in your domain and 3) as it is essentially a method call it can be reused with different inputs in different @Value SpEL injections.

Below a working example. Note that it's not Spring Security specific (not using it) neither Spring Boot specific (using it):

The POM file

Got this autogenerated from Spring Initializr not having any components added.

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.example</groupId>
    <artifactId>demo</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>jar</packaging>

    <name>demo</name>
    <description>Demo project for Spring Boot</description>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.5.9.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>


</project>

The main class DemoApplication:

package com.example.demo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;


@SpringBootApplication
public class DemoApplication {

    public static void main(String[] args) {

    SecurityConfiguration securityConfiguration = SpringApplication.run(DemoApplication.class, args).getBean(SecurityConfiguration.class);

        System.out.println(securityConfiguration.getSignKeyPassword());
    }

    @Bean
    MyCustomSpELFunctionRegister customSpelFunctionProvider() {
        return new MyCustomSpELFunctionRegister();
    }
}

The SecurityConfiguration:

As mentioned Spring Security agnostic. It is using the custom SpEL keyword resolver.

package com.example.demo;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;

@Configuration
public class SecurityConfiguration {

    @Value("#{ #mydecode( '${signkey.password}' ) }")
    String signKeyPassword;

    public String getSignKeyPassword() {
        return signKeyPassword;
    }
}

The MyCustomSpELDecoderFunction

This is the class where you'll hide your Utils static method that does the work

package com.example.demo;

public abstract class MyCustomSpELDecoderFunction {

    //needs to be public
    //alternative use interface with defined static method
    public static String mydecode(String encrypted) {
        return "myutils decrypt";
    }
}

The MyCustomSpELFunctionRegister class

This is the glue code that connects your custom SpEL keyword to your Utils class. It implements BeanFactoryPostProcessor to perform the registration before any bean gets created thus halting the @Value injection.

package com.example.demo;

import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.context.expression.StandardBeanExpressionResolver;
import org.springframework.expression.spel.support.StandardEvaluationContext;

public class MyCustomSpELFunctionRegister implements BeanFactoryPostProcessor {

    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {

        beanFactory.setBeanExpressionResolver(new StandardBeanExpressionResolver() {
            @Override
            protected void customizeEvaluationContext(StandardEvaluationContext standardEvaluationContext) {
                try {

                    //here we register all functions
                    standardEvaluationContext.registerFunction("mydecode", MyCustomSpELDecoderFunction.class.getMethod("mydecode", new Class[] { String.class }));

                } catch (NoSuchMethodException e) {
                    e.printStackTrace();
                }
            }
        });
    }
}
like image 116
dimitrisli Avatar answered Jan 28 '26 17:01

dimitrisli


Yes, it is possible as @Value may be used as meta annotation:

Option 1: Override @Value value:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
@Value("")
public @interface SignKeyPassword {

    @AliasFor(annotation=Value.class, attribute="value")
    String value() default "#{T(my.package.PasswordUtil).decode('${signkey.password}')}";

}

Option 2: Separate annotation for each property:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
@Value("#{T(my.package.PasswordUtil).decode('${signkey.password}')}")
public @interface SignKeyPassword {
}


@Configuration
@EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {

    @SignKeyPassword
    private String signKeyPassword;

}

Option 3: Implement InstantiationAwareBeanPostProcessor to define own injection login

like image 37
Anton Avatar answered Jan 28 '26 16:01

Anton