Глобальный обработчик ошибок в 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
Начать дискуссию
\n","lang":""}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

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

"}},{"type":"code","cover":false,"hidden":false,"anchor":"","data":{"text":"{\n \"error\": \"Bad Request\",\n \"message\": \"Email должен быть валидным\",\n \"timestamp\": \"2024-02-15T12:00:00\"\n}","lang":""}},{"type":"header","cover":false,"hidden":false,"anchor":"","data":{"style":"h3","text":"1.2. Защита от утечки информации"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

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

"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

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

"}},{"type":"code","cover":false,"hidden":false,"anchor":"","data":{"text":"HTTP 500 Internal Server Error\norg.hibernate.exception.SQLGrammarException: could not execute query\n...\nCaused by: org.postgresql.util.PSQLException: ERROR: syntax error at or near \"admin\"","lang":""}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

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

"}},{"type":"list","cover":false,"hidden":false,"anchor":"","data":{"items":["Используется PostgreSQL.","Есть SQL-инъекция (ошибка в запросе).","Название таблицы или поля (admin)."],"type":"UL"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

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

"}},{"type":"header","cover":false,"hidden":false,"anchor":"","data":{"style":"h3","text":"1.3. Логирование ошибок"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

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

"}},{"type":"list","cover":false,"hidden":false,"anchor":"","data":{"items":["Отладки.","Обнаружения атак (например, если кто-то пытается вызвать ошибки специально)."],"type":"UL"}},{"type":"header","cover":false,"hidden":false,"anchor":"","data":{"style":"h2","text":"2. Как это связано с безопасностью?"}},{"type":"header","cover":false,"hidden":false,"anchor":"","data":{"style":"h3","text":"2.1. Предотвращение атак через ошибки"}},{"type":"list","cover":false,"hidden":false,"anchor":"","data":{"items":["SQL-инъекции — если в ошибке показывается SQL-запрос, злоумышленник может использовать это для взлома.","Brute-force — если при логине возвращается разный текст ошибки для \"неверный логин\" и \"неверный пароль\", это помогает подбирать учётные данные.","Информационная утечка — версии библиотек, структура БД, пути файлов."],"type":"UL"}},{"type":"header","cover":false,"hidden":false,"anchor":"","data":{"style":"h3","text":"2.2. OWASP рекомендации"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

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

"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

URL:

"}},{"type":"link","cover":false,"hidden":false,"anchor":"","data":{"link":{"type":"link","data":{"url":"https://api.vc.ru/v2.8/redirect?to=https%3A%2F%2Fowasp.org%2Fwww-project-top-ten%2F&postId=1923944","title":"OWASP Top Ten | OWASP Foundation","description":"The OWASP Top 10 is the reference standard for the most critical web application security risks. Adopting the OWASP Top 10 is perhaps the most effective first step towards changing your software development culture focused on producing secure code.","image":{"type":"image","data":{"uuid":"2f3e63ed-0ee5-57c1-832a-9ae1a25a41cf","width":64,"height":64,"size":2000,"type":"png","color":"d0cece","hash":"","external_service":[]}},"v":1,"hostname":"owasp.org"}}}},{"type":"list","cover":false,"hidden":false,"anchor":"","data":{"items":["A01:2021 — Broken Access Control (неправильная обработка доступа).","A03:2021 — Injection (ошибки, связанные с SQL, NoSQL, командами ОС).","A05:2021 — Security Misconfiguration (неправильные настройки ошибок)."],"type":"UL"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

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

"}},{"type":"header","cover":false,"hidden":false,"anchor":"","data":{"style":"h2","text":"3. Настройка глобального обработчика ошибок в Spring Boot"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

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

"}},{"type":"header","cover":false,"hidden":false,"anchor":"","data":{"style":"h3","text":"3.1. Основные компоненты обработчика"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

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

"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

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

"}},{"type":"list","cover":false,"hidden":false,"anchor":"","data":{"items":["@ControllerAdvice — указывает, что класс обрабатывает исключения во всех контроллерах.","@ResponseBody — автоматически преобразует возвращаемые объекты в JSON (как у @RestController).","Централизованная обработка — все исключения из любых контроллеров будут проходить через этот класс."],"type":"OL"}},{"type":"code","cover":false,"hidden":false,"anchor":"","data":{"text":"@RestControllerAdvice\npublic class GlobalExceptionHandler {\n // Методы-обработчики...\n}","lang":""}},{"type":"header","cover":false,"hidden":false,"anchor":"","data":{"style":"h3","text":"3.2. Методы-обработчики (@ExceptionHandler)"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

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

"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

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

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

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

"}},{"type":"list","cover":false,"hidden":false,"anchor":"","data":{"items":["Если в любом контроллере выброшено UserNotFoundException, Spring вызывает этот метод.","Метод логирует событие (уровень WARN, так как это ожидаемая ошибка).","Создаётся объект ErrorResponse с удобочитаемым сообщением.","Клиент получает JSON с HTTP-статусом 404 (Not Found)."],"type":"OL"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

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

"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

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

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

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

"}},{"type":"list","cover":false,"hidden":false,"anchor":"","data":{"items":["Spring автоматически проверяет входные данные при использовании @Valid.","Если валидация не пройдена, вызывается этот метод.","ex.getBindingResult().getFieldErrors() содержит список всех ошибок.","Клиент получает 400 (Bad Request) с перечислением проблем."],"type":"OL"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

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

"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

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

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

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

"}},{"type":"list","cover":false,"hidden":false,"anchor":"","data":{"items":["Не показывайте ex.getMessage() в продакшене — это может раскрыть внутреннюю логику приложения.","Логируйте полный стек только в режиме разработки (dev)."],"type":"UL"}},{"type":"header","cover":false,"hidden":false,"anchor":"","data":{"style":"h3","text":"3.3. DTO для ошибок (ErrorResponse)"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

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

"}},{"type":"code","cover":false,"hidden":false,"anchor":"","data":{"text":"@Data // Lombok-аннотация (генерирует геттеры/сеттеры)\npublic static class ErrorResponse {\n private String error; // Тип ошибки (например, \"Validation Error\")\n private String message; // Детали (например, \"Email не может быть пустым\")\n private int status; // HTTP-статус (например, 400)\n private long timestamp; // Время возникновения ошибки\n\n public ErrorResponse(String error, String message, int status) {\n this.error = error;\n this.message = message;\n this.status = status;\n this.timestamp = System.currentTimeMillis();\n }\n}","lang":""}},{"type":"header","cover":false,"hidden":false,"anchor":"","data":{"style":"h3","text":"3.4. Настройка application.properties"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

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

"}},{"type":"code","cover":false,"hidden":false,"anchor":"","data":{"text":"# Отключаем стандартную страницу ошибок Spring Boot\nserver.error.whitelabel.enabled=false\n\n# Скрываем stacktrace в ответах\nserver.error.include-stacktrace=never\nserver.error.include-message=on_param\nserver.error.include-binding-errors=never","lang":""}},{"type":"header","cover":false,"hidden":false,"anchor":"","data":{"style":"h3","text":"3.5. Полный тестовый пример"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

HelloController:

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

GlobalExceptionHandler:

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

UserNotFoundException:

"}},{"type":"code","cover":false,"hidden":false,"anchor":"","data":{"text":"public class UserNotFoundException extends RuntimeException {\n public UserNotFoundException(String message) {\n super(message);\n }\n}","lang":""}},{"type":"header","cover":false,"hidden":false,"anchor":"","data":{"style":"h3","text":"3.6. Как проверить обработчик?"}},{"type":"media","cover":false,"hidden":false,"anchor":"","data":{"items":[{"title":"","image":{"type":"image","data":{"uuid":"558b3a4b-b331-5321-b5df-1ea58589258c","width":974,"height":234,"size":61083,"type":"png","color":"0d0d0f","hash":"","external_service":[],"base64preview":"/9j/4AAQSkZJRgABAQIAOwA7AAD/2wBDAAMCAgICAgMCAgIDAwMDBAYEBAQEBAgGBgUGCQgKCgkICQkKDA8MCgsOCwkJDRENDg8QEBEQCgwSExIQEw8QEBD/2wBDAQMDAwQDBAgEBAgQCwkLEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBD/wAARCAAKAAoDAREAAhEBAxEB/8QAFwAAAwEAAAAAAAAAAAAAAAAAAAMGCP/EAB8QAAICAgEFAAAAAAAAAAAAAAECABEDBSEEEzFhof/EABUBAQEAAAAAAAAAAAAAAAAAAAAB/8QAFBEBAAAAAAAAAAAAAAAAAAAAAP/aAAwDAQACEQMRAD8AwVjV36V1O3otQYVjBHkWfdcwFFXxk4xuVIXi+2hv5IISUED/2Q=="}}}]}},{"type":"header","cover":false,"hidden":false,"anchor":"","data":{"style":"h2","text":"Вывод"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

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

"}},{"type":"list","cover":false,"hidden":false,"anchor":"","data":{"items":["Даёт единый формат ответов.","Защищает от утечки данных (SQL-ошибки, стектрейсы).","Улучшает безопасность (скрывает детали реализации).","Позволяет логировать ошибки для анализа атак."],"type":"UL"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

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

"}},{"type":"list","cover":false,"hidden":false,"anchor":"","data":{"items":["Не показывайте ex.getMessage() в продакшене — только общие сообщения.","Логируйте ошибки, но без sensitive-данных.","Ограничивайте частые ошибки (Rate Limiting)."],"type":"OL"}}],"summaryContent":null,"isExistSummaryContent":false,"warningFromEditor":null,"warningFromEditorTitle":null,"counters":{"comments":0,"favorites":0,"reposts":0,"views":318,"hits":787,"reads":null,"online":0},"dateFavorite":0,"hitsCount":787,"isCommentsEnabled":true,"isLikesEnabled":true,"isRemovedByUserRequest":false,"isFavorited":false,"isPinned":false,"repostId":null,"repostData":null,"subscribedToTreads":false,"isEditorial":false,"isAudioAvailable":false,"audioUrl":null,"isAudioAvailableToGenerate":false,"commentEditor":{"enabled":true,"who":null,"text":"","until":null,"reason":null,"type":"everybody"},"isBlur":false,"isPublished":true,"isDisabledAd":false,"withheld":[],"ogTitle":null,"ogDescription":"Глобальный обработчик ошибок в Spring Boot, защита от утечек данных, улучшение безопасности, логирование ошибок, настройка в приложениях на Java","url":"https://vc.ru/dev/1923944-globalnyj-obrabotchik-oshibok-spring-boot-bezopasnost","author":{"id":2776479,"name":"Коротко об IT и безопасности","nickname":null,"description":null,"uri":"","avatar":{"type":"image","data":{"uuid":"38d2909a-7117-54f4-ab27-6d0ab4df9945","width":487,"height":509,"size":47188,"type":"png","color":"84d1d1","hash":"","external_service":[],"base64preview":"/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAMCAgICAgMCAgIDAwMDBAYEBAQEBAgGBgUGCQgKCgkICQkKDA8MCgsOCwkJDRENDg8QEBEQCgwSExIQEw8QEBD/2wBDAQMDAwQDBAgEBAgQCwkLEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBD/wAARCAAKAAoDAREAAhEBAxEB/8QAFQABAQAAAAAAAAAAAAAAAAAAAAf/xAAfEAABAwQDAQAAAAAAAAAAAAABAAIEAwUGERIhMUH/xAAYAQEAAwEAAAAAAAAAAAAAAAAABAUGB//EAB4RAAIBAwUAAAAAAAAAAAAAAAADAgEFEQQhIzKR/9oADAMBAAIRAxEAPwC35ZkuaW/NI1ptcWoYlZ0UR9R+bKwe8ityd8LGjfo0O+1evY+L4xh1OYW/S25tvY19eSmcb+FEU4zYQBAf/9k="}},"cover":{"cover":{"type":"image","data":{"uuid":"9d9b3e95-ec9f-5736-b6f3-bcd61ff9eb65","width":1920,"height":1080,"size":636727,"type":"png","color":"c78b6c","hash":"","external_service":[],"base64preview":"/9j/4AAQSkZJRgABAQIAJQAlAAD/2wBDAAMCAgICAgMCAgIDAwMDBAYEBAQEBAgGBgUGCQgKCgkICQkKDA8MCgsOCwkJDRENDg8QEBEQCgwSExIQEw8QEBD/2wBDAQMDAwQDBAgEBAgQCwkLEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBD/wAARCAAKAAoDAREAAhEBAxEB/8QAFgABAQEAAAAAAAAAAAAAAAAABwQG/8QAJRAAAgEDAwIHAAAAAAAAAAAAAQMCBAUGABESB1EIEyExQUJx/8QAFwEAAwEAAAAAAAAAAAAAAAAABAYHCP/EACYRAAEDAgUDBQAAAAAAAAAAAAECAxEABAUGEkGRISNhFHGxstH/2gAMAwEAAhEDEQA/AJcSw3AMXx1mS2jAF0CLfCoqbnG5VcG1NvrJbRWylSqQ5EgrkI8ZcgI9ttD2D4eRLcq/aW2l3Vvi6GLxekpIExPSTHkjnc1vLZkNoZbaRgydMxJCzyFmMBLeI9eJAMfwgbdhpUdduNZ7W9aEZc7aYGw3oIyWoqH+KrrvJ72MkijYtRnIkrj5ihxjv7Db4GmjJ4HpEex+ajuYOuIInx9aQFUtMVQJp1EmI+g1SjbMkyUDgUWl5wAAKPNf/9k="}},"cover_y":18},"achievements":[{"title":"Год на vc.ru","code":"registration_1_year","description":"Первый год с vc.ru. Получена 24 июля 2025.","previewUuid":"0d11c244-49de-50e7-894e-b9b27945d42b","formats":{"glb":"https://static.vc.ru/achievements/fish.glb","usdz":"https://static.vc.ru/achievements/fish.usdz"},"viewData":{"contentColor":"#C67AA3","textMaxWidth":0.634765625,"textX":0.5888671875,"textY":0.54296875,"logoX":0.5859375,"logoY":0.6669921875,"logoXNoText":0.6044921875,"logoYNoText":0.5439453125},"id":2697861,"userId":2776479,"count":0,"shareImage":"https://api.vc.ru/achievements/share/2697861"}],"lastModificationDate":1764931528,"isSubscribed":false,"isSubscribedToNewPosts":false,"isMuted":false,"isAvailableForMessenger":true,"badgeId":null,"isDonationsEnabled":false,"isPlusGiftEnabled":true,"isUnverifiedBlogForCompanyWithoutPro":false,"isRemovedByUserRequest":false,"isFrozen":false,"isDisabledAd":false,"isPlus":false,"isVerified":false,"isPro":false,"yandexMetricaId":null,"badge":null,"isOnline":false,"tgChannelShortname":null,"isUnsubscribable":true,"type":1,"subtype":"personal_blog"},"subsite":{"id":235819,"name":"Разработка","description":"Сообщество разработчиков: публикации о личном опыте, выдающиеся приёмы при решении рутинных задач, полезные материалы для профессионального роста.","uri":"/dev","avatar":{"type":"image","data":{"uuid":"fef5b5fb-e488-5b7f-8445-e3a26a910b44","width":1200,"height":1200,"size":7757,"type":"png","color":"343434","hash":"04042b2b1c1000","external_service":[]}},"cover":{"type":"image","data":{"uuid":"2a214cc5-35cc-58ca-bc07-fc1c892d2101","width":960,"height":280,"size":177,"type":"png","color":"343434","hash":"","external_service":[]}},"lastModificationDate":1642411346,"isSubscribed":false,"isSubscribedToNewPosts":false,"isMuted":false,"isAvailableForMessenger":false,"isDisabledAd":false,"nickname":"dev","isUnsubscribable":true,"badge":null,"badgeId":null,"isDonationsEnabled":false,"isOnline":false,"isPlus":false,"isUnverifiedBlogForCompanyWithoutPro":false,"isVerified":false,"isRemovedByUserRequest":false,"isFrozen":false,"isPro":false,"type":2,"subtype":"community"},"reactions":{"counters":[{"id":1,"count":2}],"reactionId":0},"isNews":false,"source":null,"clusters":[],"donations":{"amount":0,"isDonated":false},"commentsSeenCount":null,"keywords":[],"media":null,"customCover":null,"robotsTag":null,"categories":[10],"isAnonymized":true}};