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.
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.
Here, apply validation to check whether the target property or column of the sort condition is in the allow list.
As usual, let's create a Spring Boot development project from "SPRING INITIALIZR". At that time, select "Web" as the dependency artifact.
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
).
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 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;
}
}
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 :)
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.
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 ...)
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.
Recommended Posts