Комментарии (1)
🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
Скрытие ошибок пользователю: Best Practices
Показывать полные stack trace и внутренние ошибки пользователям — критическая ошибка безопасности и плохого UX. Это может выдать информацию о архитектуре приложения, версиях библиотек и создать дыры для атак. Правильное скрытие ошибок требует многоуровневого подхода.
Основной принцип
Правило 1: Пользователь видит дружественное сообщение Правило 2: Разработчик видит полную информацию в логах Правило 3: Never expose stack traces to clients
Решение 1: Global Exception Handler в Spring
Используй @ControllerAdvice для централизованной обработки ошибок:
// Пользовательские исключения
public class UserFacingException extends RuntimeException {
private String userMessage;
private int httpStatus;
public UserFacingException(String userMessage, int httpStatus) {
this.userMessage = userMessage;
this.httpStatus = httpStatus;
}
public String getUserMessage() {
return userMessage;
}
public int getHttpStatus() {
return httpStatus;
}
}
public class InternalException extends RuntimeException {
// Разработчикам видна полная информация
// Пользователям видно только дружественное сообщение
}
// Global Exception Handler
@ControllerAdvice
public class GlobalExceptionHandler {
private static final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);
// 1. Обработка expected ошибок
@ExceptionHandler(UserFacingException.class)
public ResponseEntity<ErrorResponse> handleUserFacingException(
UserFacingException ex) {
return ResponseEntity
.status(ex.getHttpStatus())
.body(new ErrorResponse(
ex.getUserMessage(),
null // Без stack trace для пользователя!
));
}
// 2. Обработка validation ошибок
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ErrorResponse> handleValidationException(
MethodArgumentNotValidException ex) {
String userMessage = "Ошибка валидации. Проверьте входные данные.";
// Логируем полную информацию
logger.warn("Validation error: {}", ex.getBindingResult().getAllErrors());
return ResponseEntity
.status(HttpStatus.BAD_REQUEST)
.body(new ErrorResponse(
userMessage,
null
));
}
// 3. Обработка неожиданных ошибок
@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleGenericException(
Exception ex) {
String errorId = UUID.randomUUID().toString();
// ВСЕГДА логируй полный stack trace для разработчиков
logger.error("Unexpected error [{}]", errorId, ex);
// Пользователю показываем дружественное сообщение
return ResponseEntity
.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(new ErrorResponse(
"Произошла ошибка на сервере. Попробуйте позже.",
errorId // Для связи с логами
));
}
}
// DTO для ответа
public class ErrorResponse {
private String message;
private String errorId; // Для трейсинга в логах
public ErrorResponse(String message, String errorId) {
this.message = message;
this.errorId = errorId;
}
public String getMessage() { return message; }
public String getErrorId() { return errorId; }
}
Решение 2: Разделение типов исключений
// Исключения, которые показываем пользователю
public class ValidationException extends Exception {
public ValidationException(String message) {
super(message); // Пользователь видит это
}
}
public class ResourceNotFoundException extends Exception {
public ResourceNotFoundException(String resourceType, String id) {
super("" + resourceType + " с ID " + id + " не найден");
}
}
// Исключения, которые скрываем
public class DatabaseException extends Exception {
// Пользователь НЕ должен видеть детали БД
}
public class ExternalApiException extends Exception {
// Пользователь НЕ должен видеть детали внешних сервисов
}
// Обработка
@RestController
public class UserController {
@GetMapping("/users/{id}")
public ResponseEntity<User> getUser(@PathVariable String id) {
try {
return ResponseEntity.ok(userService.findById(id));
} catch (ValidationException ex) {
// Показываем пользователю
return ResponseEntity
.status(HttpStatus.BAD_REQUEST)
.body(new ErrorResponse(ex.getMessage(), null));
} catch (ResourceNotFoundException ex) {
// Показываем пользователю
return ResponseEntity
.status(HttpStatus.NOT_FOUND)
.body(new ErrorResponse(ex.getMessage(), null));
} catch (DatabaseException ex) {
// Логируем, но НЕ показываем
logger.error("Database error:", ex);
return ResponseEntity
.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(new ErrorResponse(
"Произошла ошибка. Попробуйте позже.",
null
));
}
}
}
Решение 3: Логирование с разными уровнями
@Service
public class UserService {
private static final Logger logger = LoggerFactory.getLogger(UserService.class);
public User findById(String id) {
try {
// Логика
validateId(id);
return userRepository.findById(id)
.orElseThrow(() ->
new ResourceNotFoundException("User", id)
);
} catch (DatabaseException ex) {
// ERROR: с полным stack trace для разработчиков
logger.error("Failed to fetch user from database for id: {}", id, ex);
// Но выбрасываем дружественное исключение
throw new InternalException("Database error");
} catch (ValidationException ex) {
// WARN: это expected case
logger.warn("Invalid user ID: {}", id);
throw ex;
}
}
private void validateId(String id) {
if (id == null || id.isEmpty()) {
throw new ValidationException("ID не может быть пустым");
}
}
}
Решение 4: Чувствительная информация в логах
@Service
public class PaymentService {
private static final Logger logger = LoggerFactory.getLogger(PaymentService.class);
public void processPayment(PaymentRequest request) {
try {
// НЕ логируй полные данные карты!
String maskedCard = maskCardNumber(request.getCardNumber());
logger.info("Processing payment for card: {}", maskedCard);
// Обработка платежа
externalPaymentGateway.charge(request);
} catch (PaymentGatewayException ex) {
// Логируем для разработчиков, но маскируем чувствительное
logger.error(
"Payment failed for card: {}, error code: {}",
maskCardNumber(request.getCardNumber()),
ex.getErrorCode(),
ex // Полный stack trace
);
// Пользователю показываем дружественное сообщение
throw new UserFacingException(
"Платёж не прошёл. Проверьте данные карты.",
400
);
}
}
private String maskCardNumber(String cardNumber) {
// Показываем только последние 4 цифры
return "****-****-****-" + cardNumber.substring(cardNumber.length() - 4);
}
}
Решение 5: Environment-specific error responses
@Configuration
public class ErrorHandlingConfig {
@Value("${app.debug-mode:false}")
private boolean debugMode;
@Bean
public ErrorResponseFactory errorResponseFactory() {
return new ErrorResponseFactory(debugMode);
}
}
public class ErrorResponseFactory {
private boolean debugMode;
public ErrorResponseFactory(boolean debugMode) {
this.debugMode = debugMode;
}
public ErrorResponse createErrorResponse(
Exception ex,
String userFriendlyMessage) {
if (debugMode) {
// DEV: показываем всё
return new ErrorResponse(
ex.getMessage(),
ex.getStackTrace().toString()
);
} else {
// PROD: скрываем детали
return new ErrorResponse(
userFriendlyMessage,
null
);
}
}
}
// application-prod.yml
app:
debug-mode: false
// application-dev.yml
app:
debug-mode: true
Решение 6: Security headers для минимизации информации
@Configuration
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
// Скрывай информацию о сервере
.headers(h -> h
.contentSecurityPolicy("default-src self")
.xssProtection()
.frameOptions()
)
// Не показывай подробности аутентификации
.authenticationEntryPoint((request, response, authException) -> {
response.setStatus(401);
response.setContentType("application/json");
response.getWriter().write("{\"error\": \"Unauthorized\"}");
});
return http.build();
}
}
Чеклист безопасности
- Никогда не выводи stack trace в ответе пользователю
- Всегда логируй полную информацию для разработчиков
- Используй errorId для связи UI логов и server логов
- Не выводи информацию о версиях библиотек
- Маскируй чувствительные данные (карты, пароли, токены)
- Используй разные сообщения для разных типов ошибок
- В PROD отключай debug mode
- Регулярно проверяй логи на утечку информации
- Используй централизованный Exception Handler
- Валидируй входные данные и показывай понятные ошибки
Вывод
Скрытие ошибок от пользователя — это баланс между:
- Безопасностью: не выдавать архитектурные детали
- UX: показывать понятные сообщения
- Дебаггингом: сохранять полную информацию в логах
Используй GlobalExceptionHandler + разумное логирование и твоё приложение будет безопаснее и удобнее.