Глобальный обработчик ошибок в Spring Boot, и причем тут безопасность?

Салимжанов Р.

Введение

Когда вы разрабатываете веб-приложение, ошибки неизбежны. Пользователь может ввести неверные данные, сервер временно недоступен, или в коде возникнет непредвиденная ситуация. Если приложение не обрабатывает ошибки правильно, это может привести к:

  • Утечке конфиденциальной информации (например, паролей, SQL-запросов, структуры базы данных).
  • Неудобству для пользователей (непонятные сообщения об ошибках или технические детали).
  • Уязвимостям безопасности (злоумышленники могут использовать ошибки для атак).

Глобальный обработчик ошибок (Global Exception Handler) — это механизм Java в Spring Boot, который позволяет централизованно перехватывать и обрабатывать все исключения в приложении. В этой статье мы разберём:

  1. Зачем нужен глобальный обработчик ошибок?
  2. Как он связан с безопасностью?
  3. Пошаговая настройка в Spring Boot (с кодом).

1. Зачем нужен глобальный обработчик ошибок?

1.1. Единый формат ответов

Без обработчика разные ошибки могут возвращать ответы в разном формате:

❌ Плохо:

// Ошибка валидации { "timestamp": "2024-02-15T12:00:00", "status": 400, "error": "Bad Request", "message": "Validation failed for object='user'" } // Ошибка 500 (внутренняя ошибка сервера) <!DOCTYPE html> <html> <head> <title>Error 500</title> </head> <body> Internal Server Error: NullPointerException in UserController.java:42 </body> </html>

✅ Хорошо (с обработчиком):

{ "error": "Bad Request", "message": "Email должен быть валидным", "timestamp": "2024-02-15T12:00:00" }

1.2. Защита от утечки информации

По умолчанию Spring Boot может показывать стектрейс (stack trace) ошибки, что опасно:

🚨 Пример уязвимости:

HTTP 500 Internal Server Error org.hibernate.exception.SQLGrammarException: could not execute query ... Caused by: org.postgresql.util.PSQLException: ERROR: syntax error at or near "admin"

Злоумышленник видит:

  • Используется PostgreSQL.
  • Есть SQL-инъекция (ошибка в запросе).
  • Название таблицы или поля (admin).

✅ С обработчиком: Мы скрываем технические детали и возвращаем только безопасное сообщение.

1.3. Логирование ошибок

Глобальный обработчик позволяет централизованно логировать ошибки, что полезно для:

  • Отладки.
  • Обнаружения атак (например, если кто-то пытается вызвать ошибки специально).

2. Как это связано с безопасностью?

2.1. Предотвращение атак через ошибки

  • SQL-инъекции — если в ошибке показывается SQL-запрос, злоумышленник может использовать это для взлома.
  • Brute-force — если при логине возвращается разный текст ошибки для "неверный логин" и "неверный пароль", это помогает подбирать учётные данные.
  • Информационная утечка — версии библиотек, структура БД, пути файлов.

2.2. OWASP рекомендации

По стандартам OWASP Топ 10:

URL:

  • A01:2021 — Broken Access Control (неправильная обработка доступа).
  • A03:2021 — Injection (ошибки, связанные с SQL, NoSQL, командами ОС).
  • A05:2021 — Security Misconfiguration (неправильные настройки ошибок).

✅ Решение: Глобальный обработчик скрывает опасные детали.

3. Настройка глобального обработчика ошибок в Spring Boot

В этом разделе мы разберём, как именно работает глобальный обработчик исключений в Spring Boot, как его правильно настроить и какие компоненты за что отвечают.

3.1. Основные компоненты обработчика

@RestControllerAdvice — главная аннотация

Эта аннотация объединяет три функции:

  1. @ControllerAdvice — указывает, что класс обрабатывает исключения во всех контроллерах.
  2. @ResponseBody — автоматически преобразует возвращаемые объекты в JSON (как у @RestController).
  3. Централизованная обработка — все исключения из любых контроллеров будут проходить через этот класс.
@RestControllerAdvice public class GlobalExceptionHandler { // Методы-обработчики... }

3.2. Методы-обработчики (@ExceptionHandler)

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

Пример 1: Обработка кастомных исключений

@ExceptionHandler(UserNotFoundException.class) public ResponseEntity<ErrorResponse> handleUserNotFound(UserNotFoundException ex) { // Логирование logger.warn("User not found: {}", ex.getMessage()); // Формирование ответа ErrorResponse error = new ErrorResponse( "Not Found", ex.getMessage(), // Сообщение из исключения HttpStatus.NOT_FOUND.value() ); // Возврат HTTP 404 return new ResponseEntity<>(error, HttpStatus.NOT_FOUND); }

Как это работает:

  1. Если в любом контроллере выброшено UserNotFoundException, Spring вызывает этот метод.
  2. Метод логирует событие (уровень WARN, так как это ожидаемая ошибка).
  3. Создаётся объект ErrorResponse с удобочитаемым сообщением.
  4. Клиент получает JSON с HTTP-статусом 404 (Not Found).

Пример 2: Обработка ошибок валидации (@Valid)

Когда данные не проходят валидацию (например, поле email пустое), Spring выбрасывает MethodArgumentNotValidException.

@ExceptionHandler(MethodArgumentNotValidException.class) public ResponseEntity<ErrorResponse> handleValidationErrors(MethodArgumentNotValidException ex) { // Сбор всех ошибок в одну строку String errorMessage = ex.getBindingResult() .getFieldErrors() .stream() .map(FieldError::getDefaultMessage) .collect(Collectors.joining(", ")); logger.warn("Validation error: {}", errorMessage); ErrorResponse error = new ErrorResponse( "Validation Error", errorMessage, // Например: "Email не может быть пустым, Пароль слишком короткий" HttpStatus.BAD_REQUEST.value() ); return new ResponseEntity<>(error, HttpStatus.BAD_REQUEST); }

Как это работает:

  1. Spring автоматически проверяет входные данные при использовании @Valid.
  2. Если валидация не пройдена, вызывается этот метод.
  3. ex.getBindingResult().getFieldErrors() содержит список всех ошибок.
  4. Клиент получает 400 (Bad Request) с перечислением проблем.

Пример 3: Перехват всех необработанных исключений

Этот метод — "ловушка" для всех ошибок, которые не были перехвачены другими обработчиками.

@ExceptionHandler(Exception.class) public ResponseEntity<ErrorResponse> handleAllExceptions(Exception ex) { // Важно: в продакшене не логируем полный stacktrace! logger.error("Internal error: {}", ex.getMessage(), ex); ErrorResponse error = new ErrorResponse( "Internal Server Error", "Произошла непредвиденная ошибка. Обратитесь в поддержку.", // Не показываем ex.getMessage()! HttpStatus.INTERNAL_SERVER_ERROR.value() ); return new ResponseEntity<>(error, HttpStatus.INTERNAL_SERVER_ERROR); }

Важные нюансы:

  • Не показывайте ex.getMessage() в продакшене — это может раскрыть внутреннюю логику приложения.
  • Логируйте полный стек только в режиме разработки (dev).

3.3. DTO для ошибок (ErrorResponse)

Этот класс определяет структуру JSON-ответа при ошибках.

@Data // Lombok-аннотация (генерирует геттеры/сеттеры) public static class ErrorResponse { private String error; // Тип ошибки (например, "Validation Error") private String message; // Детали (например, "Email не может быть пустым") private int status; // HTTP-статус (например, 400) private long timestamp; // Время возникновения ошибки public ErrorResponse(String error, String message, int status) { this.error = error; this.message = message; this.status = status; this.timestamp = System.currentTimeMillis(); } }

3.4. Настройка application.properties

Добавьте эти строки, чтобы отключить опасные настройки Spring Boot по умолчанию:

# Отключаем стандартную страницу ошибок Spring Boot server.error.whitelabel.enabled=false # Скрываем stacktrace в ответах server.error.include-stacktrace=never server.error.include-message=on_param server.error.include-binding-errors=never

3.5. Полный тестовый пример

HelloController:

import jakarta.validation.Valid; import jakarta.validation.constraints.NotBlank; import org.springframework.web.bind.annotation.*; @RestController @RequestMapping("/api") public class HelloController { @GetMapping("/hello") public String getMessage() { return "Привет, это бекенд!"; } @GetMapping("/user/{id}") public String getUser(@PathVariable int id) { if (id == 0) { throw new UserNotFoundException("Пользователь с ID " + id + " не найден"); } return "Пользователь #" + id; } @PostMapping("/validate") public String validateInput(@Valid @RequestBody ValidRequest request) { return "Валидация пройдена: " + request.getName(); } // DTO для валидации public static class ValidRequest { @NotBlank(message = "Имя не может быть пустым") private String name; // Геттеры и сеттеры public String getName() { return name; } public void setName(String name) { this.name = name; } } }

GlobalExceptionHandler:

import lombok.Data; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.validation.FieldError; import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; import java.util.stream.Collectors; @RestControllerAdvice public class GlobalExceptionHandler { private static final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class); // Обработка кастомных исключений (например, "Пользователь не найден") @ExceptionHandler(UserNotFoundException.class) public ResponseEntity<ErrorResponse> handleUserNotFound(UserNotFoundException ex) { logger.warn("User not found: {}", ex.getMessage()); ErrorResponse error = new ErrorResponse( "Not Found", ex.getMessage(), HttpStatus.NOT_FOUND.value() ); return new ResponseEntity<>(error, HttpStatus.NOT_FOUND); } // Обработка ошибок валидации (@Valid) @ExceptionHandler(MethodArgumentNotValidException.class) public ResponseEntity<ErrorResponse> handleValidationErrors(MethodArgumentNotValidException ex) { String errorMessage = ex.getBindingResult() .getFieldErrors() .stream() .map(FieldError::getDefaultMessage) .collect(Collectors.joining(", ")); logger.warn("Validation error: {}", errorMessage); ErrorResponse error = new ErrorResponse( "Validation Error", errorMessage, HttpStatus.BAD_REQUEST.value() ); return new ResponseEntity<>(error, HttpStatus.BAD_REQUEST); } // Обработка всех остальных ошибок (500 Internal Server Error) @ExceptionHandler(Exception.class) public ResponseEntity<ErrorResponse> handleAllExceptions(Exception ex) { logger.error("Internal error: {}", ex.getMessage(), ex); // Логируем полный стек только для разработки ErrorResponse error = new ErrorResponse( "Internal Server Error", "Произошла непредвиденная ошибка. Обратитесь в поддержку.", HttpStatus.INTERNAL_SERVER_ERROR.value() ); return new ResponseEntity<>(error, HttpStatus.INTERNAL_SERVER_ERROR); } // DTO для ошибок @Data public static class ErrorResponse { private String error; private String message; private int status; private long timestamp = System.currentTimeMillis(); public ErrorResponse(String error, String message, int status) { this.error = error; this.message = message; this.status = status; } } }

UserNotFoundException:

public class UserNotFoundException extends RuntimeException { public UserNotFoundException(String message) { super(message); } }

3.6. Как проверить обработчик?

Глобальный обработчик ошибок в Spring Boot, и причем тут безопасность?

Вывод

✅ Глобальный обработчик ошибок в Spring Boot:

  • Даёт единый формат ответов.
  • Защищает от утечки данных (SQL-ошибки, стектрейсы).
  • Улучшает безопасность (скрывает детали реализации).
  • Позволяет логировать ошибки для анализа атак.

Рекомендации:

  1. Не показывайте ex.getMessage() в продакшене — только общие сообщения.
  2. Логируйте ошибки, но без sensitive-данных.
  3. Ограничивайте частые ошибки (Rate Limiting).
2
Начать дискуссию