[JAVA] J'ai besoin de la validation de Spring Data pour Pageable ~

Cette fois, je vais vous présenter comment effectuer la validation (contrôle d'entrée) sur la valeur détenue par l'interface Pageable fournie par Spring Data. En utilisant Pageable fourni par Spring Data, en plus des informations de page à rechercher (nombre de pages, nombre d'éléments affichés sur la page), les conditions de tri (propriété / colonne cible, ordre croissant / décroissant) peuvent être spécifiées dans les paramètres de la requête. Il est fonctionnellement pratique de pouvoir spécifier la condition de tri, mais si vous ne l'utilisez pas correctement, cela peut provoquer une injection SQL et ainsi de suite. Ce qu'il faut pour empêcher ce genre d'injection, c'est ... inutile de dire ... la validation.

Version de vérification de fonctionnement

Méthode de réalisation

Validez les arguments de la méthode Handler implémentée dans le Controller à l'aide de la fonction Method Validation (utilisant le mécanisme de validation Bean) fournie par Spring.

Plus précisément ... ça ressemble à ça.

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

La validation de méthode fournie par Spring utilise la fonctionnalité de Spring AOP pour prendre en charge la validation des arguments d'appel de méthode et des valeurs de retour.

Regardons ça! !!

Ici, appliquez une validation qui vérifie si la propriété cible ou la colonne de la condition de tri se trouve dans la liste d'autorisation.

Créer un projet de développement

Comme toujours, créons un projet de développement Spring Boot à partir de "SPRING INITIALIZR". Dans ce cas, sélectionnez "Web" comme artefact de dépendance.

Application de Spring Data Commons

Ajoutez Spring Data Commons, qui contient «Pageable», aux bibliothèques dépendantes.

pom.xml


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

Lorsque vous ajoutez Spring Data Commons à vos bibliothèques dépendantes, le mécanisme AutoConfigure de Spring Boot active des fonctionnalités qui vous permettent de spécifier Pageable comme argument de la méthode du gestionnaire du contrôleur (comme PageableHandlerMethodArgumentResolver).

Créer un ConstraintValidator

Créez une "annotation de contrainte" et une "classe d'implémentation pour ConstraintValidator" pour vérifier "Pageable".

Annotation de contrainte pour 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();
    }

}

Classe d'implémentation de ConstraintValidator pour 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;
    }

}

Appliquer la validation de méthode

Appliquez la validation de méthode à l'appel de méthode du gestionnaire du contrôleur.

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 //Définition du bean du composant qui génère le validateur de la validation du bean
    static LocalValidatorFactoryBean localValidatorFactoryBean() {
        return new LocalValidatorFactoryBean();
    }

    @Bean // Method Validation(AOP)Définition du bean du composant auquel
    static MethodValidationPostProcessor methodValidationPostProcessor(LocalValidatorFactoryBean localValidatorFactoryBean) {
        MethodValidationPostProcessor processor = new MethodValidationPostProcessor();
        processor.setValidator(localValidatorFactoryBean);
        return processor;
    }

}

Créer une méthode Handler

Créez une méthode de gestion qui reçoit Pageable et spécifiez l'annotation de contrainte créée dans ↑. À ce moment, il est nécessaire d'ajouter «@ Validated» au niveau de la classe pour être la cible de la validation de méthode.

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 //Si vous ne l'ajoutez pas, il ne sera pas soumis à la validation de méthode
@RestController
@RequestMapping("/todos")
public class TodoRestController {

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

}

Si c'est vrai, le processus de recherche est exécuté et la liste des objets de domaine est retournée, mais ici l'implémentation est telle que le "Pageable" reçu est retourné tel quel. (Parce que c'est une application de vérification uniquement ...: sweat_smile :)

Essayez de bouger

Commençons par l'application Spring Boot et y accédons-y.

Comportement lorsqu'un nom de colonne de propriété dans la liste d'autorisation est spécifié


$ 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}

Comportement lorsqu'un nom de colonne de propriété qui n'est pas dans la liste d'autorisation est spécifié


$ 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"}

Cela semble fonctionner correctement, mais l'erreur lors de la spécification d'un nom de colonne de propriété qui n'est pas dans la liste d'autorisation est traitée comme une erreur de serveur interne 500. Normalement, ce devrait être 400 Bad Request.

Faisons une manipulation exceptionnelle! !!

Dans l'état par défaut, ce sera 500 Erreur de serveur interne, ajoutons donc la gestion des exceptions lorsqu'une erreur se produit dans la validation de méthode pour en faire 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 //Ajout d'une méthode pour gérer ConstraintViolationException
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    String handleConstraintViolationException(ConstraintViolationException e) {
        return "Detect invalid parameter.";
    }

}

À l'origine, il devrait être implémenté dans le gestionnaire d'exceptions global, mais ici, il est implémenté dans le contrôleur. En outre, la réponse d'erreur est implémentée de sorte qu'elle ne génère qu'un simple message d'erreur, donc implémentez-la selon vos besoins.

Réponse d'erreur après l'ajout de la gestion des exceptions


$ 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:

Si l'application est bien conçue, je ne pense pas que l'utilisateur saisira manuellement les informations gérées par Pageable, donc je ne pense pas qu'il soit nécessaire de renvoyer une réponse d'erreur aimable lorsqu'une erreur est détectée. (Ce type d'erreur ne devrait pas se produire si vous utilisez la fonction fournie par l'application ...)

Résumé

Cette fois, j'ai présenté comment valider l'interface Pageable fournie par Spring Data. Je ne sais pas s'il est préférable d'utiliser la validation de méthode, mais je pense que cette méthode est recommandée pour le moment car elle peut être vérifiée de la même manière que la fonction de validation de Spring MVC.

Site de référence

Recommended Posts

J'ai besoin de la validation de Spring Data pour Pageable ~
Mémorandum WebMvcConfigurer de Spring Boot 2.0 (printemps 5)
J'ai essayé Spring Data JDBC 1.0.0.BUILD-SNAPSHOT (-> 1.0.0.RELEASE)
Exemple de code pour la recherche à l'aide de QBE (requête par exemple) de Spring Data JPA
Résumé de ce que j'ai appris sur Spring Boot
Gestion collective des erreurs de validation Spring avec @ControllerAdvice
Résumé de ce que j'ai appris dans Spring Batch
Personnalisation de la validation
J'ai essayé Spring.
J'ai créé une image Docker pour la version japonaise de SDAPS
Exécution asynchrone des requêtes examinée dans Spring Boot 1.5.9
[Pour les débutants] DI ~ Les bases de DI et DI au printemps ~
05. J'ai essayé de supprimer la source de Spring Boot
J'ai essayé de réduire la capacité de Spring Boot
J'ai essayé de démarrer avec Spring Data JPA
Jusqu'à l'utilisation de Spring Data et JPA Part 2
Explication de Ruby sur rails pour les débutants ⑥ ~ Création de validation ~