[JAVA] I need validation of Spring Data for Pageable ~

This time, I will introduce how to perform validation (input check) on the value held by the Pageable interface provided by Spring Data. By using Pageable provided by Spring Data, in addition to the page information to be searched (number of pages, number of items displayed in the page), sort conditions (target property / column, ascending / descending order) can be specified in the request parameters. It is functionally convenient to be able to specify sort conditions, but if you use it incorrectly, it can cause SQL injection and so on. What is needed to prevent this kind of injection is ... needless to say ... validation.

Operation verification version

Realization method

Validate the arguments of the Handler method implemented in Controller using the Method Validation (utilizing the Bean Validation mechanism) function provided by Spring.

Specifically ... it looks like this.

@GetMapping
Page<Todo> search(@AllowedSortProperties({"id", "title"}) Pageable pageable) {
    // ...
    return page;
}

Method Validation provided by Spring uses the functionality of Spring AOP to support validation of method call arguments and return values.

Let's check it out! !!

Here, apply validation to check whether the target property or column of the sort condition is in the allow list.

Creating a development project

As usual, let's create a Spring Boot development project from "SPRING INITIALIZR". At that time, select "Web" as the dependency artifact.

Applying Spring Data Commons

Add Spring Data Commons, which contains Pageable, to the dependent libraries.

pom.xml


<dependency>
	<groupId>org.springframework.data</groupId>
	<artifactId>spring-data-commons</artifactId>
</dependency>

When you add Spring Data Commons to your dependent libraries, Spring Boot's AutoConfigure mechanism enables features that allow you to specify Pageable as an argument to the Controller's Handler method (such as PageableHandlerMethodArgumentResolver).

Creating a ConstraintValidator

Create "Constraint annotation" and "Implementation class of ConstraintValidator "to check forPageable.

Constraint annotation for Pageable


package com.example;

import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;

import static java.lang.annotation.ElementType.ANNOTATION_TYPE;
import static java.lang.annotation.ElementType.CONSTRUCTOR;
import static java.lang.annotation.ElementType.PARAMETER;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

@Documented
@Constraint(validatedBy = {AllowedSortPropertiesValidator.class})
@Target({ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER})
@Retention(RUNTIME)
public @interface AllowedSortProperties {
    String message() default "{com.example.AllowedSortProperties.message}";

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};

    String[] value();

    @Target({ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER})
    @Retention(RUNTIME)
    @Documented
    @interface List {
        AllowedSortProperties[] value();
    }

}

Implementation class of ConstraintValidator for Pageable


package com.example;

import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;

import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;

public class AllowedSortPropertiesValidator implements ConstraintValidator<AllowedSortProperties, Pageable> {

    private Set<String> allowedSortProperties;

    @Override
    public void initialize(AllowedSortProperties constraintAnnotation) {
        this.allowedSortProperties = new HashSet<>(Arrays.asList(constraintAnnotation.value()));
    }

    @Override
    public boolean isValid(Pageable value, ConstraintValidatorContext context) {
        if (value == null || value.getSort() == null) {
            return true;
        }
        if (allowedSortProperties.isEmpty()) {
            return true;
        }
        for (Sort.Order order : value.getSort()) {
            if (!allowedSortProperties.contains(order.getProperty())) {
                return false;
            }
        }
        return true;
    }

}

Apply Method Validation

Apply Method Validation to the Controller's Handler method call.

package com.example;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;
import org.springframework.validation.beanvalidation.MethodValidationPostProcessor;

@SpringBootApplication
public class BeanValidationDemoApplication {

    public static void main(String[] args) {
        SpringApplication.run(BeanValidationDemoApplication.class, args);
    }

    @Bean //Bean definition of component that generates Validator for Bean Validation
    static LocalValidatorFactoryBean localValidatorFactoryBean() {
        return new LocalValidatorFactoryBean();
    }

    @Bean // Method Validation(AOP)Bean definition of the component to which
    static MethodValidationPostProcessor methodValidationPostProcessor(LocalValidatorFactoryBean localValidatorFactoryBean) {
        MethodValidationPostProcessor processor = new MethodValidationPostProcessor();
        processor.setValidator(localValidatorFactoryBean);
        return processor;
    }

}

Creating a Handler method

Create a Handler Method that receives Pageable and specify the constraint annotation created in ↑. At that time, it is necessary to add @Validated to the class level in order to be the target of Method Validation.

package com.example;

import org.springframework.data.domain.Pageable;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@Validated //If you do not add this, it will not be subject to Method Validation
@RestController
@RequestMapping("/todos")
public class TodoRestController {

    @GetMapping
    Pageable search(@AllowedSortProperties({"id", "title"}) Pageable pageable) {
        return pageable;
    }

}

If it is true, the search process is executed and the list of domain objects is returned, but here the implementation is such that the received Pageable is returned as it is. (Because it is a verification app ...: sweat_smile :)

Try to move

Let's start the Spring Boot application and actually access it.

Behavior when the property column name in the allow list is specified


$ curl -D - http://localhost:8080/todos?sort=id
HTTP/1.1 200 
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Date: Sat, 17 Dec 2016 15:51:51 GMT

{"sort":[{"direction":"ASC","property":"id","ignoreCase":false,"nullHandling":"NATIVE","ascending":true}],"offset":0,"pageNumber":0,"pageSize":20}

Behavior when a property column name that is not in the allow list is specified


$ curl -D - http://localhost:8080/todos?sort=createdAt
HTTP/1.1 500 
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Date: Sat, 17 Dec 2016 15:53:47 GMT
Connection: close

{"timestamp":1481990027135,"status":500,"error":"Internal Server Error","exception":"javax.validation.ConstraintViolationException","message":"No message available","path":"/todos"}

It seems to work fine, but the error when specifying a property column name that is not in the allow list is treated as a 500 Internal Server Error. Normally, it should be 400 Bad Request.

Let's do exceptional handling! !!

In the default state, it will be 500 Internal Server Error, so let's add exception handling when an error occurs in Method Validation and make it 400 Bad Request.

package com.example;

import org.springframework.data.domain.Pageable;
import org.springframework.http.HttpStatus;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;

import javax.validation.ConstraintViolationException;

@Validated
@RestController
@RequestMapping("/todos")
public class TodoRestController {

    @GetMapping
    Pageable search(@AllowedSortProperties({"id", "title"}) Pageable pageable) {
        return pageable;
    }

    @ExceptionHandler //Added method to handle ConstraintViolationException
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    String handleConstraintViolationException(ConstraintViolationException e) {
        return "Detect invalid parameter.";
    }

}

Originally it should be implemented in the global exception handler, but here it is implemented in the Controller. Also, the error response is implemented so that it only outputs a simple error message, so implement it according to your requirements.

Error response after adding exception handling


$ curl -D - http://localhost:8080/todos?sort=createdAt
HTTP/1.1 400 
Content-Type: text/plain;charset=UTF-8
Content-Length: 25
Date: Sat, 17 Dec 2016 16:02:29 GMT
Connection: close

Detect invalid parameter.

Note:

If the application is well designed, I don't think the user will manually enter the information managed by Pageable, so I don't think it is necessary to return a kind error response when an error is detected. (This kind of error should not occur if you are using the function provided by the application ...)

Summary

This time, I introduced how to validate the Pageable interface provided by Spring Data. I don't know if it's best practice to use Method Validation, but I think this method is recommended at the moment because it can be checked in the same way as the validation function of Spring MVC.

Reference site

Recommended Posts

I need validation of Spring Data for Pageable ~
Summary of rails validation (for myself)
WebMvcConfigurer Memorandum of Understanding for Spring Boot 2.0 (Spring 5)
I tried Spring Data JDBC 1.0.0.BUILD-SNAPSHOT (-> 1.0.0.RELEASE)
Sample code for search using QBE (Query by Example) of Spring Data JPA
Summary of what I learned about Spring Boot
Collective handling of Spring validation errors with @ControllerAdvice
Summary of what I learned in Spring Batch
Customization of validation
I tried Spring.
I made a Docker image of SDAPS for Japanese
I checked asynchronous execution of queries in Spring Boot 1.5.9
[For beginners] DI ~ The basics of DI and DI in Spring ~
05. I tried to stub the source of Spring Boot
I tried to reduce the capacity of Spring Boot
I tried to get started with Spring Data JPA
Until the use of Spring Data and JPA Part 2
Explanation of Ruby on rails for beginners ⑥ ~ Creation of validation ~