Рубрики
Без рубрики

Проверка тела запроса и параметра в Spring Boot

Этот пост был первоначально опубликован на https://blog.tericcabrel.com Никогда не доверяйте пользовательскому вводу A… Помечен как spring boot, java, бэкэнд.

Этот пост был первоначально опубликован на Этот пост был первоначально опубликован на

Никогда не доверяйте пользовательскому вводу

Типичный рабочий процесс веб-приложения таков: получите запрос с некоторыми входными данными, выполните обработку с полученными входными данными и, наконец, верните ответ. При разработке нашего приложения мы обычно тестируем только “счастливый путь” или часто думаем, что конечный пользователь не может предоставить плохие входные данные. Чтобы предотвратить это, мы можем выполнить проверку входных данных перед обработкой запроса.

Весна предлагает элегантный способ сделать это, и мы посмотрим, как это сделать.

Пример использования

Нам нужно создать систему, в которой пользователь мог бы забронировать номер в отеле. Пользователь должен предоставить информацию о своем адресе при регистрации. Возможными действиями являются:

  • Зарегистрируйте пользователя с его адресом.
  • Сделайте резервацию для существующего пользователя.

Ниже приведена диаграмма взаимосвязи сущностей системы, созданная с помощью необработанного SQL :

Предпосылки

Для этого урока вам необходимо, чтобы на вашем компьютере были установлены Java 11 и MySQL или Docker если вы не хотите устанавливать MySQL на свой компьютер. Действительно, вы можете запустить контейнер Docker из образа докера MySQL. Этого будет достаточно для достижения цели.

docker run -it -e MYSQL_ROOT_PASSWORD=secretpswd -e MYSQL_DATABASE=hotels --name hotels-mysql -p 3307:3306 mysql:8.0

Настройка проекта

Давайте создадим новый весенний проект из start.spring.io с необходимыми зависимостями.

Зависимость, ответственная за проверку входных данных, – это проверка компонента с помощью валидатора гибернации . Обратите внимание, что эта библиотека не имеет связи с аспектом сохранения в режиме гибернации, предоставленным здесь Spring Data JPA. Откройте проект в своей среде разработки и задайте порт сервера и учетные данные базы данных в файле application.properties .

server.port=4650

spring.datasource.url=jdbc:mysql://localhost:3307/hotels?serverTimezone=UTC&useSSL=false
spring.datasource.username=root
spring.datasource.password=secretpswd

## Hibernate properties
spring.jpa.hibernate.use-new-id-generator-mappings=false
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=false
spring.jpa.open-in-view=false

Создание сущностей и сервисов

Нам нужно создать сущности Пользователь, Адрес и бронирование . Для каждой сущности мы создадим соответствующий репозиторий и Сервис. Поскольку это не основная тема данного урока, найдите код этих файлов в репозитории Github :

  • Сущности находятся внутри пакета модели .
  • Репозитории сущностей находятся внутри пакета репозитории .
  • Услуги входят в пакет услуги .

Зарегистрировать пользователя

Нам нужно создать конечную точку для обработки этого действия, а также определить объект, который будет получать входные данные, необходимые для создания пользователя.

Создайте модель для тела запроса

При регистрации нового пользователя мы также предоставляем информацию о его адресе в теле. Нам нужно структурировать наш объект, чтобы справиться с этим, создав класс addressdto, который будет содержать все свойства для адреса, и внутри класса RegisterUserDTO будет свойство типа addressdto.

Имена классов имеют суффикс DTO (Объект передачи данных) потому что они переносят данные с одного уровня (контроллер) на другой уровень (постоянство). Распространенный случай его использования – это когда нам нужно применить некоторые данные преобразования, прежде чем передавать их на другой уровень. Я напишу более подробный пост об этом позже.

Создайте пакет с именем dto внутри пакета модели , затем создайте два класса AddressDto.java и RegisterUserDto.java .

Добавить проверку

Hibernate Validator предоставляет встроенные ограничения, которые будут использоваться для проверки наших входных данных. Чтобы ознакомиться с полным списком, перейдите по этой ссылке | .

Чтобы ознакомиться с полным списком, перейдите по этой ссылке ||| .

package com.tericcabrel.hotel.models.dtos;

import com.tericcabrel.hotel.models.Address;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Pattern;
import javax.validation.constraints.Pattern.Flag;
import lombok.Data;

@Data
public class AddressDto {
  @NotBlank(message = "The country is required.")
  private String country;

  @NotBlank(message = "The city is required.")
  private String city;

  @NotBlank(message = "The Zip code is required.")
  @Pattern(regexp = "^\\d{1,5}$", flags = { Flag.CASE_INSENSITIVE, Flag.MULTILINE }, message = "The Zip code is invalid.")
  private String zipCode;

  @NotBlank(message = "The street name is required.")
  private String street;

  private String state;

  public Address toAddress() {
    return new Address()
        .setCountry(country)
        .setCity(city)
        .setZipCode(zipCode)
        .setStreet(street)
        .setState(state);
  }
}

Чтобы ознакомиться с полным списком, перейдите по этой ссылке ||| .

package com.tericcabrel.hotel.models.dtos;

import com.tericcabrel.hotel.models.User;
import java.util.Date;
import javax.validation.Valid;
import javax.validation.constraints.Email;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Past;
import javax.validation.constraints.Pattern.Flag;
import javax.validation.constraints.Size;
import lombok.Data;

@Data
public class RegisterUserDto {
  @NotEmpty(message = "The full name is required.")
  @Size(min = 2, max = 100, message = "The length of full name must be between 2 and 100 characters.")
  private String fullName;

  @NotEmpty(message = "The email address is required.")
  @Email(message = "The email address is invalid.", flags = { Flag.CASE_INSENSITIVE })
  private String email;

  @NotNull(message = "The date of birth is required.")
  @Past(message = "The date of birth must be in the past.")
  private Date dateOfBirth;

  @NotEmpty(message = "The gender is required.")
  private String gender;

  @Valid
  @NotNull(message = "The address is required.")
  private AddressDto address;

  public User toUser() {
    return new User()
        .setName(fullName)
        .setEmail(email.toLowerCase())
        .setBirthDate(dateOfBirth)
        .setGender(gender)
        .setAddress(address.toAddress());
  }
}

Создайте маршрут для тестовой регистрации

Давайте создадим конечную точку, ответственную за регистрацию нового пользователя. Создайте пакет с именем контроллеры , затем создайте контроллер с именем UserController.java . Добавьте код ниже:

package com.tericcabrel.hotel.controllers;

/*      CLASSES IMPORT HERE        */

@RequestMapping(value = "/user")
@RestController
public class UserController {
  private final UserService userService;

  public UserController(UserService userService) {
    this.userService = userService;
  }

  @PostMapping("/register")
  public ResponseEntity registerUser(@Valid @RequestBody RegisterUserDto registerUserDto) {
    User createdUser = userService.create(registerUserDto.toUser());

    return new ResponseEntity<>(createdUser, HttpStatus.CREATED);
  }
}

Наиболее важной частью приведенного выше кода является использование аннотации @Valid .

Когда Spring находит аргумент, помеченный @Valid, он автоматически проверяет аргумент и выдает исключение, если проверка завершается неудачно .

Запустите приложение и убедитесь, что при запуске нет ошибок.

Тест с почтальоном

Наше приложение запущено; откройте postman и отправьте запрос со всеми входными данными в null и посмотрите результат.

Мы получили статус HTTP 400 с сообщением “Неверный запрос”, не более того.. Давайте проверим консоль приложений, чтобы посмотреть, что произошло:

Как мы видим, исключение типа MethodArgumentNotValidException было выдано исключение, но, поскольку исключение нигде не перехвачено, ответ является ответом на неправильный запрос.

Обрабатывать исключение ошибки проверки

Spring предоставляет специализированную аннотацию @Component , называемую @ControllerAdvice , которая позволяет обрабатывать исключения, создаваемые методами, аннотированными @RequestMapping и аналогичными в одном глобальном компоненте.

Создайте пакет с именем исключения , затем создайте файл с именем GlobalExceptionHandler.java . Добавьте код ниже:

package com.tericcabrel.hotel.exceptions;

import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import org.springframework.context.support.DefaultMessageSourceResolvable;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.context.request.WebRequest;
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;

@ControllerAdvice
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {
  @Override
  protected ResponseEntity handleMethodArgumentNotValid(
      MethodArgumentNotValidException ex, HttpHeaders headers,
      HttpStatus status, WebRequest request) {

    Map> body = new HashMap<>();

    List errors = ex.getBindingResult()
        .getFieldErrors()
        .stream()
        .map(DefaultMessageSourceResolvable::getDefaultMessage)
        .collect(Collectors.toList());

    body.put("errors", errors);

    return new ResponseEntity<>(body, HttpStatus.BAD_REQUEST);
  }
}

Запустите приложение и позвоните почтальону.

Проверьте дату рождения в функции и почтовый индекс буквами алфавита (почтовый индекс во Франции не может содержать букв).

Создать резервацию

Давайте сделаем то же самое, создав CreateReservationDto.java затем добавьте код ниже:

package com.tericcabrel.hotel.models.dtos;

import com.tericcabrel.hotel.models.Reservation;
import java.util.Date;
import javax.validation.constraints.FutureOrPresent;
import javax.validation.constraints.Max;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Positive;
import lombok.Data;

@Data
public class CreateReservationDto {
  @NotNull(message = "The number of bags is required.")
  @Min(value = 1, message = "The number of bags must be greater than 0")
  @Max(value = 3, message = "The number of bags must be greater than 3")
  private int bagsCount;

  @NotNull(message = "The departure date is required.")
  @FutureOrPresent(message = "The departure date must be today or in the future.")
  private Date departureDate;

  @NotNull(message = "The arrival date is required.")
  @FutureOrPresent(message = "The arrival date must be today or in the future.")
  private Date arrivalDate;

  @NotNull(message = "The room's number is required.")
  @Positive(message = "The room's number must be greater than 0")
  private int roomNumber;

  @NotNull(message = "The extras is required.")
  @NotEmpty(message = "The extras must have at least one item.")
  private String[] extras;

  @NotNull(message = "The user's Id is required.")
  @Positive(message = "The user's Id must be greater than 0")
  private int userId;

  private String note;

  public Reservation toReservation() {
    return new Reservation()
        .setBagsCount(bagsCount)
        .setDepartureDate(departureDate)
        .setArrivalDate(arrivalDate)
        .setRoomNumber(roomNumber)
        .setExtras(extras)
        .setNote(note);
  }
}

Найдите код для ReservationController.java в репозитории исходного кода.

Тест с почтальоном

Проверить параметр запроса

Теперь мы хотим получить бронирование с помощью сгенерированного уникального кода.

Конечная точка будет выглядеть следующим образом: /резервирование/RSV-2021-1001 . Поскольку код бронирования предоставлен пользователем, нам необходимо проверить его, чтобы избежать ненужного вызова базы данных, потому что, если предоставленный код не в этом хорошем формате, мы уверены, что он не будет найден в базе данных.

При создании маршрута с помощью Spring можно добавить правило аннотации для проверки входных данных. В нашем случае мы применим регулярное выражение, а затем проверим формат кода бронирования.

Внутри ReservationController.java , добавьте код ниже:

@GetMapping("/{code}")
public ResponseEntity oneReservation(@Pattern(regexp = "^RSV(-\\d{4,}){2}$") @PathVariable String code)
      throws ResourceNotFoundException {
    Optional optionalReservation = reservationService.findByCode(code);

    if (optionalReservation.isEmpty()) {
      throw new ResourceNotFoundException("No reservation found with the code: " + code);
    }

    return new ResponseEntity<>(optionalReservation.get(), HttpStatus.OK);
}

Если вы запустите приложение и вызовете маршрут с неправильным кодом, ничего не произойдет, потому что нам нужно сообщить контроллеру о проверке параметров с помощью правил аннотаций. Мы достигаем этого с помощью аннотации @Validated , которая добавляется в класс контроллера резервирования:

@Validated
@RequestMapping(value = "/reservations")
@RestController
public class ReservationController {
    // code here
}

Теперь запустите приложение и протестируйте его с помощью неверного кода бронирования.

Упс!!! Приложение выдает внутреннюю ошибку сервера, и консоль выглядит следующим образом:

Проверка завершилась неудачей, как и ожидалось, но было выдано новое исключение типа ConstraintViolationException . Поскольку приложение не улавливает его, возвращается внутренняя ошибка сервера. Обновите GlobalExceptionHandler.java чтобы поймать это исключение:

@ExceptionHandler(ConstraintViolationException.class)
public ResponseEntity constraintViolationException(ConstraintViolationException ex, WebRequest request) {
    List errors = new ArrayList<>();

    ex.getConstraintViolations().forEach(cv -> errors.add(cv.getMessage()));

    Map> result = new HashMap<>();
    result.put("errors", errors);

    return new ResponseEntity<>(result, HttpStatus.BAD_REQUEST);
}

Запустите приложение и протестируйте. Теперь мы получили ошибку с четким сообщением.

Вывод

Мы подошли к концу этого урока, и теперь мы можем реализовать надежную проверку, которая сделала наш бэкэнд более устойчивым к неправильным вводным данным, поступающим от пользователей. Мы использовали предопределенные правила проверки на протяжении всего урока, но мы также можем создать собственное правило проверки для специализированного варианта использования.

Чтобы узнать, как написать собственное правило проверки, ознакомьтесь эта ссылка .

Найдите код этого руководства в репозитории Github .

Оригинал: “https://dev.to/tericcabrel/validate-request-body-and-parameter-in-spring-boot-1fai”