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

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.