Custom converter for @RequestParam in Spring MVC
Asked Answered
F

4

15

I am getting an encrypted String as Query parameter to a Spring rest controller method.

I wanted to decrypt the string before it reaches the method based on some annotation (say @Decrypt) like below

@RequestMapping(value = "/customer", method = RequestMethod.GET)
public String getAppointmentsForDay(@RequestParam("secret") @Decrypt String customerSecret) {
    System.out.println(customerSecret);  // Needs to be a decrypted value.
   ...
}

Is a custom Formatter the right approach in this use case?

Or should I use a custom HandlerMethodArgumentResolver?

Flemish answered 13/10, 2017 at 11:33 Comment(0)
D
20

A custom implementation of org.springframework.format.Formatter is a valid approach for this use case. This is how Spring itself implements formatters for dates, currencies, number styles etc.

Steps:

  1. Declare an annotation: Decrypt:

    import java.lang.annotation.*;
    
    @Documented
    @Retention(RetentionPolicy.RUNTIME)
    @Target({ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER, ElementType.ANNOTATION_TYPE})
    public @interface Decrypt {
    
    }
    
  2. Declare an AnnotationFormatterFactory which uses the new annotation:

    import org.springframework.context.support.EmbeddedValueResolutionSupport;
    import org.springframework.format.AnnotationFormatterFactory;
    import org.springframework.format.Formatter;
    import org.springframework.format.Parser;
    import org.springframework.format.Printer;
    
    import java.text.ParseException;
    import java.util.Collections;
    import java.util.HashSet;
    import java.util.Locale;
    import java.util.Set;
    
    public class DecryptAnnotationFormatterFactory extends EmbeddedValueResolutionSupport
            implements AnnotationFormatterFactory<Decrypt> {
    
        @Override
        public Set<Class<?>> getFieldTypes() {
            Set<Class<?>> fieldTypes = new HashSet<>();
            fieldTypes.add(String.class);
            return Collections.unmodifiableSet(fieldTypes);
        }
    
        @Override
        public Printer<String> getPrinter(Decrypt annotation, Class<?> fieldType) {
            return configureFormatterFrom(annotation);
        }
    
        @Override
        public Parser<String> getParser(Decrypt annotation, Class<?> fieldType) {
            return configureFormatterFrom(annotation);
        }
    
        private Formatter<String> configureFormatterFrom(Decrypt annotation) {
            // you could model something on the Decrypt annotation for use in the decryption call
            // in this example the 'decryption' call is stubbed, it just reverses the given String
            // presumaby you implementaion of this Formatter will be different e.g. it will invoke your encryption routine
            return new Formatter<String>() {
                @Override
                public String print(String object, Locale locale) {
                    return object;
                }
    
                @Override
                public String parse(String text, Locale locale) throws ParseException {
                    return new StringBuilder(text).reverse().toString();
                }
            };
        }
    }
    
  3. Register this formatter factory with your web context:

    import org.springframework.context.annotation.Configuration;
    import org.springframework.format.FormatterRegistry;
    import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
    
    @Configuration
    public class WebConfigurer extends WebMvcConfigurerAdapter {
        @Override
        public void addFormatters(FormatterRegistry registry) {
            super.addFormatters(registry);
            registry.addFormatterForFieldAnnotation(new DecryptAnnotationFormatterFactory());
        }
    }
    
  4. That's it.

With the above in place, all usages of a @RequestParam which are qualified with @Decrypt will be passed through the parse() method declared in DecryptAnnotationFormatterFactory so you can implement your decryption call there.

To prove this, the following test passes:

@RunWith(SpringRunner.class)
@WebMvcTest(controllers = YourController.class)
public class YourControllerTest {
    @Autowired
    private MockMvc mockMvc;

    @Test
    public void theSecretRequestParameterWillBeConverted() throws Exception {
        MvcResult mvcResult = mockMvc.perform(get("/customer?secret=abcdef")).andExpect(status().isOk()).andReturn();

        // the current implementation of the 'custom' endpoint returns the value if the secret request parameter and
        // the current decrypt implementation just reverses the given value ...
        assertThat(mvcResult.getResponse().getContentAsString(), is("fedcba"));
    }
}
Dermatoplasty answered 17/10, 2017 at 15:0 Comment(2)
Thanks glitch for the response, I am trying this and below approachFlemish
no need for extends EmbeddedValueResolutionSupportSkipton
O
8

HandlerMethodArgumentResolver would be the best in this regard.

  1. Create your annotation:

@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Decrypt {
    String value();
}
  1. Create your Custom HandlerMethodArgumentResolver:

public class DecryptResolver implements HandlerMethodArgumentResolver {

    @Override
    public boolean supportsParameter(MethodParameter parameter) {
        return parameter.getParameterAnnotation(Decrypt.class) != null;
    }

    @Override
    public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest,
            WebDataBinderFactory binderFactory) throws Exception {
        Decrypt attr = parameter.getParameterAnnotation(Decrypt.class);
        String encrypted = webRequest.getParameter(attr.value());
        String decrypted = decrypt(encrypted);

        return decrypted;
    }

    private String decrypt(String encryptedString) {
        // Your decryption logic here

        return "decrypted - "+encryptedString;
    }
}
  1. Register the resolver:

@Configuration
@EnableMvc // If you're not using Spring boot
public class WebConfig extends WebMvcConfigurerAdapter {

    @Override
    public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
          argumentResolvers.add(new DecryptResolver());
    }
}
  1. Voila, you have your decrypted parameter. Note that you won't need to use @RequestParam anymore.

@RequestMapping(value = "/customer", method = RequestMethod.GET)
public String getAppointmentsForDay(@Decrypt("secret") String customerSecret) {
System.out.println(customerSecret);  // Needs to be a decrypted value.
   ...
}
Overgrowth answered 17/10, 2017 at 21:47 Comment(1)
Thanks @Overgrowth for the response, I am trying this and above approachFlemish
T
0

You can try by adding a CharacterEncodingFilter with init-param encoding UTF-8 in web.xml file. Check out this example.

However If it still doesn't work, you can force encoding by adding the below param along with above init-param.

<init-param>
        <param-name>forceEncoding</param-name>
        <param-value>true</param-value>
</init-param>

Let me know If It works for you.

Theoretician answered 16/10, 2017 at 10:31 Comment(0)
C
0

If you wrap your decrypted data in a data transfer object, you can use Spring's Converter framework.

public class Decrypted { // data transfer object

    private final String value;

    public Decrypted(String value){
        this.value = value;
    }

    public String get(){
        return value;
    }
}

Implement the interface org.springframework.core.convert.converter.Converter, and add the @Component annotation.

@Component
public class DecryptConverter implements Converter<String, Decrypted> {
    @Override
    public Decrypted convert(String value) {
        return new Decrypted(decrypt(value)); // you implement "decrypt"
    }
}

And register it

@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Override
    public void addFormatters(FormatterRegistry registry) {
        registry.addConverter(new DecryptConverter());
    }
}

Then use the wrapper type (Decrypted) for the parameter:

@RequestMapping(value = "/customer", method = RequestMethod.GET)
public String getAppointmentsForDay(@RequestParam("secret") Decrypted customerSecret) {
    System.out.println(customerSecret.get());  // decrypted value
}

See also https://www.baeldung.com/spring-enum-request-param

Caylacaylor answered 6/7, 2023 at 16:0 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.