Статья по Bean Validation (Bean Validation Cheat Sheet)

Введение

Здесь собраны понятные и практичные рекомендации по использованию Bean Validation для безопасности приложений на Java.

Bean Validation (также Jakarta Validation) — один из самых распространённых способов проверки ввода в Java. Это спецификация, не привязанная к конкретному слою приложения: вы задаёте ограничения валидации на доменной модели и выполняете проверку на разных уровнях архитектуры.

Плюс такого подхода — ограничения и валидаторы описываются один раз, дублирование снижается, правила единообразны:

Типичная валидация

Типичная валидация

Bean Validation

Bean Validation

Настройка

В примерах используется Hibernate Validator.

Добавьте Hibernate Validator в pom.xml:

<dependency>
   <groupId>org.hibernate</groupId>
   <artifactId>hibernate-validator</artifactId>
   <version>USE_LATEST_VERSION</version>
</dependency>

Включите поддержку bean validation в context.xml Spring:

<beans:beans ...>
   ...
   <mvc:annotation-driven />
   ...
</beans:beans>

Подробнее — руководство по установке.

Основы

Чтобы начать пользоваться Bean Validation, добавьте к модели ограничения (@Pattern, @Digits, @Min, @Max, @Size, @Past, @Future, @CreditCardNumber, @Email, @URL и др.) и используйте аннотацию @Valid при передаче модели между слоями приложения.

Ограничения можно навешивать на:

  • поля;
  • свойства (геттеры/сеттеры);
  • классы.

В Bean Validation 1.1 также на:

  • параметры;
  • возвращаемые значения;
  • конструкторы.

Для краткости ниже — ограничения на полях, валидация вызывается из контроллера. Полный перечень сценариев см. в документации Bean Validation.

Обработка ошибок: Hibernate Validator возвращает BindingResult с List<ObjectError>. Примеры упрощены; в продакшене обычно добавляют логирование и перенаправление на страницы ошибок.

Встроенные ограничения

@Pattern

Аннотация: @Pattern(regex=,flag=)

Тип данных: CharSequence

Назначение: проверяет, что строка соответствует регулярному выражению regex с учётом флагов. Готовые шаблоны — в OWASP Validation Regex Repository.

Документация: Hibernate Validator

Модель:

import org.hibernate.validator.constraints.Pattern;

public class Article {
 // только буквы, цифры и пробел в заголовке
 @Pattern(regexp = "[a-zA-Z0-9 ]")
 private String articleTitle;
 public String getArticleTitle() {
  return articleTitle;
 }
 public void setArticleTitle(String articleTitle) {
  this.articleTitle = articleTitle;
 }

 ...

}

Контроллер:

import javax.validation.Valid;
import com.company.app.model.Article;

@Controller
public class ArticleController {

 @RequestMapping(value = "/postArticle", method = RequestMethod.POST)
 public @ResponseBody String postArticle(@Valid Article article,
 BindingResult result, HttpServletResponse response) {
  if (result.hasErrors()) {
   String errorMessage = "";
   response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
   List<ObjectError> errors = result.getAllErrors();
   for (ObjectError e : errors) {
    errorMessage += "ERROR: " + e.getDefaultMessage();
   }
   return errorMessage;
  } else {
   return "Validation Successful";
  }
 }
}

@Digits

Аннотация: @Digits(integer=,fraction=)

Тип данных: BigDecimal, BigInteger, CharSequence, byte, short, int, long и обёртки; в Hibernate Validator также подтипы Number.

Назначение: проверяет, что значение — число с не более чем integer цифрами в целой части и fraction знаками после запятой.

Документация: Hibernate Validator

Модель:

import org.hibernate.validator.constraints.Digits;

public class Customer {
  // возраст — не более 3 цифр в целой части, дробная часть 0
  @Digits(integer = 3, fraction = 0)
  private int age;

  public int getAge() {
    return age;
  }

  public void setAge(int age) {
      this.age = age;
    }

    ...

}

Контроллер:

import javax.validation.Valid;
import com.company.app.model.Customer;

@Controller
public class CustomerController {

 @RequestMapping(value = "/registerCustomer", method = RequestMethod.POST)
 public @ResponseBody String registerCustomer(@Valid Customer customer, BindingResult result,
 HttpServletResponse response) {

  if (result.hasErrors()) {
   String errorMessage = "";
   response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
   List<ObjectError> errors = result.getAllErrors();

   for (ObjectError e : errors) {
    errorMessage += "ERROR: " + e.getDefaultMessage();
   }
   return errorMessage;
  } else {
   return "Validation Successful";
  }
 }
}

@Size

Аннотация: @Size(min=, max=)

Тип данных: CharSequence, Collection, Map и массивы.

Назначение: размер элемента от min до max включительно.

Документация: Hibernate Validator

Модель:

import org.hibernate.validator.constraints.Size;

public class Message {

   // длина сообщения от 10 до 500 символов
   @Size(min = 10, max = 500)
   private String message;

   public String getMessage() {
      return message;
   }

   public void setMessage(String message) {
      this.message = message;
   }

...
}

Контроллер:

import javax.validation.Valid;
import com.company.app.model.Message;

@Controller
public class MessageController {

...

@RequestMapping(value="/sendMessage", method=RequestMethod.POST)
public @ResponseBody String sendMessage(@Valid Message message, BindingResult result,
HttpServletResponse response){

   if(result.hasErrors()){
      String errorMessage = "";
      response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
      List<ObjectError> errors = result.getAllErrors();
      for( ObjectError e : errors){
         errorMessage+= "ERROR: " + e.getDefaultMessage();
      }
      return errorMessage;
   }
   else{
      return "Validation Successful";
   }
}
}

@Past / @Future

Аннотация: @Past, @Future

Тип данных: java.util.Date, java.util.Calendar, java.time.chrono.ChronoZonedDateTime, java.time.Instant, java.time.OffsetDateTime

Назначение: дата в прошлом или в будущем.

Документация: Hibernate Validator

Модель:

import org.hibernate.validator.constraints.Past;
import org.hibernate.validator.constraints.Future;

public class DoctorVisit {

   // дата рождения — в прошлом
   @Past
   private Date birthDate;

   public Date getBirthDate() {
      return birthDate;
   }

   public void setBirthDate(Date birthDate) {
      this.birthDate = birthDate;
   }

   // запланированный визит — в будущем
   @Future
   private String scheduledVisitDate;

   public String getScheduledVisitDate() {
      return scheduledVisitDate;
   }

   public void setScheduledVisitDate(String scheduledVisitDate) {
      this.scheduledVisitDate = scheduledVisitDate;
   }

...
}

Контроллер:

import javax.validation.Valid;
import com.company.app.model.DoctorVisit;

@Controller
public class DoctorVisitController {

   ...

   @RequestMapping(value="/scheduleVisit", method=RequestMethod.POST)
   public @ResponseBody String scheduleVisit(@Valid DoctorVisit doctorvisit, BindingResult result,
   HttpServletResponse response){

      if(result.hasErrors()){
         String errorMessage = "";
         response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
         List<ObjectError> errors = result.getAllErrors();
         for( ObjectError e : errors){
            errorMessage+= "ERROR: " + e.getDefaultMessage();
         }
         return errorMessage;
      }
      else{
         return "Validation Successful";
      }
   }
}

Комбинирование ограничений

Аннотации валидации можно сочетать. Например, оценка reviewRating от 1 до 5:

Аннотации: @Min(value=), @Max(value=)

Тип данных: BigDecimal, BigInteger, byte, short, int, long и обёртки; в Hibernate Validator также подтипы CharSequence (интерпретируется числовое значение строки) и Number.

Назначение: значение не ниже минимума и не выше максимума (включительно).

Документация: Hibernate Validator

Модель:

import org.hibernate.validator.constraints.Min;
import org.hibernate.validator.constraints.Max;

public class Review {

 @Min(1)
 @Max(5)
 private int reviewRating;

 public int getReviewRating() {
  return reviewRating;
 }

 public void setReviewRating(int reviewRating) {
  this.reviewRating = reviewRating;
 }

 ...
}

Контроллер:

import javax.validation.Valid;
import com.company.app.model.ReviewRating;

@Controller
public class ReviewController {

   ...

   @RequestMapping(value="/postReview", method=RequestMethod.POST)
   public @ResponseBody String postReview(@Valid Review review, BindingResult result,
   HttpServletResponse response){

      if(result.hasErrors()){
         String errorMessage = "";
         response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
         List<ObjectError> errors = result.getAllErrors();
         for( ObjectError e : errors){
            errorMessage+= "ERROR: " + e.getDefaultMessage();
         }
         return errorMessage;
      }
      else{
         return "Validation Successful";
      }
   }
}

Каскадная валидация

Проверка одного bean — хорошее начало, но объекты часто вложены или образуют граф. Чтобы проверить граф за один проход, используйте каскадную валидацию с @Valid.

Дополнительные ограничения Hibernate

Помимо набора ограничений JSR 303, Hibernate Validator добавляет, в частности:

  • @CreditCardNumber
  • @EAN
  • @Email
  • @Length
  • @Range
  • @ScriptAssert
  • @URL

Полный список — в документации.

Ограничение @SafeHtml объявлено устаревшим (см. заметку о релизах Hibernate Validator 6.1.0.Final и 6.0.18.Final); не используйте @SafeHtml.

Пользовательские ограничения

Сильная сторона Bean Validation — собственные ограничения сверх встроенных.

Создание custom constraints выходит за рамки этой статьи; см. документацию Hibernate Validator.

Сообщения об ошибках

В аннотации можно указать идентификатор сообщения для локализации и кастомизации текста ошибки:

@Pattern(regexp = "[a-zA-Z0-9 ]", message="article.title.error")
private String articleTitle;

Spring MVC подставит текст по ключу article.title.error из настроенного MessageSource. Подробнее — в материале Silver Bay Tech.

© Перевод на русский язык. Оригинальные материалы: OWASP Cheat Sheet Series.
Этот проект использует материалы OWASP, распространяемые по лицензии CC BY-SA 4.0.