Коротко об IT и безопасности

+47
с 2024
11 подписчиков
1 подписка

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

1

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

Миграции базы данных — важный процесс в разработке, позволяющий управлять изменениями структуры БД. Flyway — популярный инструмент для версионирования и автоматизации миграций.

1

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

Разделение бэкенда и фронтенда — ключевой шаг для создания масштабируемых и поддерживаемых приложений. Но как организовать их так, чтобы не запутаться в десятках файлов и сервисов? В этой статье вы узнаете, как использовать монорепозиторий и Docker для изоляции компонентов, упрощения разработки и быстрого развёртывания.

1
\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":1764922259,"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}},{"type":"entry","data":{"id":1916577,"customUri":"migratsii-bd-s-flyway-v-java-bezopasnost-i-nastroyka-v-spring-boot","subsiteId":2776479,"title":"Миграции БД с Flyway в Java: безопасность и практическое применение","date":1744209942,"dateModified":1744209942,"blocks":[{"type":"text","cover":true,"hidden":false,"anchor":"","data":{"text":"

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

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

Миграции базы данных — важный процесс в разработке, позволяющий управлять изменениями структуры БД. Flyway — популярный инструмент для версионирования и автоматизации миграций.

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

Он позволяет:

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

✅ Автоматизировать обновление структуры БД

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

✅ Контролировать версионность изменений

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

✅ Обеспечивать безопасность миграций

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

В этой статье мы разберём:

"}},{"type":"list","cover":false,"hidden":false,"anchor":"","data":{"items":["Как настроить Flyway в Spring Boot.","Пример миграции с кодом.","Почему Flyway безопаснее ручных SQL-скриптов?"],"type":"OL"}},{"type":"header","cover":false,"hidden":false,"anchor":"","data":{"style":"h2","text":"1. Настройка Flyway в Spring Boot"}},{"type":"header","cover":false,"hidden":false,"anchor":"","data":{"style":"h3","text":"1.1. Добавляем зависимость"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

В pom.xml (Maven):

"}},{"type":"code","cover":false,"hidden":false,"anchor":"","data":{"text":"\n\torg.flywaydb\n\tflyway-core\n\t10.20.1\n\n\n\torg.flywaydb\n\tflyway-database-postgresql\n\t10.20.1\n","lang":""}},{"type":"header","cover":false,"hidden":false,"anchor":"","data":{"style":"h3","text":"1.2. Настройка БД"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

В application.properties:

"}},{"type":"code","cover":false,"hidden":false,"anchor":"","data":{"text":"В application.properties:\nserver.port=8082\n\n# DB\nspring.datasource.url=jdbc:postgresql://localhost:5432/basetest\nspring.datasource.username=postgres\nspring.datasource.password=postgres\n\n# Hibernate \nspring.jpa.hibernate.ddl-auto=validate\nspring.jpa.show-sql=true\nspring.jpa.properties.hibernate.format_sql=true\n\n# Flyway Configuration\nspring.flyway.enabled=true\nspring.flyway.baseline-on-migrate=true\nspring.flyway.locations=classpath:db/migration\nlogging.level.org.flywaydb=DEBUG\nlogging.level.org.springframework.jdbc=DEBUG","lang":""}},{"type":"header","cover":false,"hidden":false,"anchor":"","data":{"style":"h3","text":"1.3. Конфигурация Flyway"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Класс FlywayConfig настраивает Flyway для работы с базой данных:

"}},{"type":"code","cover":false,"hidden":false,"anchor":"","data":{"text":"@Configuration\npublic class FlywayConfig {\n \n @Bean\n public Flyway flyway(DataSource dataSource) {\n return Flyway.configure()\n .dataSource(dataSource) // Источник данных (БД)\n .baselineOnMigrate(true) // Создаёт baseline при первом запуске\n .baselineVersion(\"0\") // Начальная версия миграций\n .validateOnMigrate(false) // Отключает валидацию (для гибкости)\n .outOfOrder(true) // Разрешает применять миграции не по порядку\n .load();\n }\n}","lang":""}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Ключевые параметры:

"}},{"type":"list","cover":false,"hidden":false,"anchor":"","data":{"items":["baselineOnMigrate — автоматически создаёт базовую версию, если БД новая.","validateOnMigrate — если false, Flyway пропустит проверку целостности миграций (полезно при разработке).","outOfOrder — разрешает применять миграции не в порядке версий."],"type":"UL"}},{"type":"header","cover":false,"hidden":false,"anchor":"","data":{"style":"h3","text":"1.4. Запуск миграций"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Класс FlywayRunner выполняет миграции и логирует процесс:

"}},{"type":"code","cover":false,"hidden":false,"anchor":"","data":{"text":"@Component\npublic class FlywayRunner implements CommandLineRunner {\n \n private final Flyway flyway;\n\n public FlywayRunner(Flyway flyway) {\n this.flyway = flyway;\n }\n\n @Override\n public void run(String... args) {\n System.out.println(\"=== Flyway migration start ===\");\n System.out.println(\"Flyway version: \" + Flyway.class.getPackage().getImplementationVersion());\n \n // Применяем миграции\n MigrateResult migrationsApplied = flyway.migrate();\n System.out.println(\"Applied \" + migrationsApplied.migrationsExecuted + \" migrations\");\n\n // Выводим список применённых миграций\n System.out.println(\"=== Applied migrations ===\");\n for (MigrationInfo info : flyway.info().applied()) {\n System.out.println(info.getVersion() + \" - \" + info.getDescription());\n }\n }\n}","lang":""}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Что делает этот код?

"}},{"type":"list","cover":false,"hidden":false,"anchor":"","data":{"items":["Запускает миграции (flyway.migrate()).","Выводит количество применённых изменений.","Логирует список выполненных миграций."],"type":"OL"}},{"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. Создаём SQL-скрипты"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Flyway требует, чтобы миграции лежали в src/main/resources/db/migration/:

"}},{"type":"media","cover":false,"hidden":false,"anchor":"","data":{"items":[{"title":"","image":{"type":"image","data":{"uuid":"ae71f2d6-ce03-5408-88ef-d92c27081a32","width":559,"height":339,"size":80741,"type":"png","color":"2d2c34","hash":"","external_service":[],"base64preview":"/9j/4AAQSkZJRgABAQIAOwA7AAD/2wBDAAMCAgICAgMCAgIDAwMDBAYEBAQEBAgGBgUGCQgKCgkICQkKDA8MCgsOCwkJDRENDg8QEBEQCgwSExIQEw8QEBD/2wBDAQMDAwQDBAgEBAgQCwkLEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBD/wAARCAAKAAoDAREAAhEBAxEB/8QAFgABAQEAAAAAAAAAAAAAAAAABgQI/8QAIhAAAQMDAwUAAAAAAAAAAAAAAQIDBAAFERIhMSRBQnGS/8QAFwEBAAMAAAAAAAAAAAAAAAAAAAECA//EABURAQEAAAAAAAAAAAAAAAAAAAAB/9oADAMBAAIRAxEAPwDHD8Bh5tMtlK3AsAkNY2Pfb3W1UQm3zcnEV35qAptjbYtsYhCQSg5255pCC65UrUrqXeT5mg//2Q=="}}}]}},{"type":"header","cover":false,"hidden":false,"anchor":"","data":{"style":"h3","text":"Первая миграция (V1__Create_users_table.sql)"}},{"type":"code","cover":false,"hidden":false,"anchor":"","data":{"text":"CREATE TABLE users (\n id SERIAL PRIMARY KEY,\n username VARCHAR(50) NOT NULL,\n password VARCHAR(100) NOT NULL\n);","lang":""}},{"type":"header","cover":false,"hidden":false,"anchor":"","data":{"style":"h3","text":"Вторая миграция (V2__Add_email_column.sql)"}},{"type":"code","cover":false,"hidden":false,"anchor":"","data":{"text":"ALTER TABLE users ADD COLUMN email VARCHAR(100);","lang":""}},{"type":"header","cover":false,"hidden":false,"anchor":"","data":{"style":"h3","text":"2.2. Запуск приложения"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

При старте Spring Boot:

"}},{"type":"list","cover":false,"hidden":false,"anchor":"","data":{"items":["Flyway проверит, есть ли таблица flyway_schema_history.","Если нет — создаст её.","Применит все не применённые миграции в порядке версий."],"type":"OL"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

После запуска в БД появится:

"}},{"type":"list","cover":false,"hidden":false,"anchor":"","data":{"items":["Таблица users (из V1).","Столбец email (из V2).","Таблица flyway_schema_history (журнал миграций)."],"type":"UL"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Как видим, до запуска нет таблиц:

"}},{"type":"media","cover":false,"hidden":false,"anchor":"","data":{"items":[{"title":"","image":{"type":"image","data":{"uuid":"b370b235-99df-56ab-8e74-10267cc2ba4c","width":614,"height":286,"size":105914,"type":"png","color":"313138","hash":"","external_service":[],"base64preview":"/9j/4AAQSkZJRgABAQIAOwA7AAD/2wBDAAMCAgICAgMCAgIDAwMDBAYEBAQEBAgGBgUGCQgKCgkICQkKDA8MCgsOCwkJDRENDg8QEBEQCgwSExIQEw8QEBD/2wBDAQMDAwQDBAgEBAgQCwkLEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBD/wAARCAAKAAoDAREAAhEBAxEB/8QAFgABAQEAAAAAAAAAAAAAAAAABQYI/8QAIxAAAQIGAAcAAAAAAAAAAAAAAgABAwQFBhQhESIjMWGBov/EABcBAQEBAQAAAAAAAAAAAAAAAAABAgP/xAAXEQEBAQEAAAAAAAAAAAAAAAAAAREh/9oADAMBAAIRAxEAPwDLdRrdwMMPAITEnIX6UP13Fl3uy5WJ0Jm3O2uHwCmhaZAMSd5G0OteCUEWg//Z"}}}]}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

После запуска:

"}},{"type":"media","cover":false,"hidden":false,"anchor":"","data":{"items":[{"title":"","image":{"type":"image","data":{"uuid":"532af65d-6192-5f50-88ef-32c11e590fdd","width":974,"height":116,"size":72304,"type":"png","color":"1c1d25","hash":"","external_service":[],"base64preview":"/9j/4AAQSkZJRgABAQIAOwA7AAD/2wBDAAMCAgICAgMCAgIDAwMDBAYEBAQEBAgGBgUGCQgKCgkICQkKDA8MCgsOCwkJDRENDg8QEBEQCgwSExIQEw8QEBD/2wBDAQMDAwQDBAgEBAgQCwkLEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBD/wAARCAAKAAoDAREAAhEBAxEB/8QAFwAAAwEAAAAAAAAAAAAAAAAAAAMFCP/EACIQAAEDAwMFAAAAAAAAAAAAAAEAAgMEBRESQVETITFh0f/EABcBAAMBAAAAAAAAAAAAAAAAAAACAwT/xAAfEQACAQIHAAAAAAAAAAAAAAAAAQMCERIUMVFhodH/2gAMAwEAAhEDEQA/AML1Ut/r5evJb5S4bhsh8H2SrJ2ISRKTUSW3bPe2vzvlr/qbHwSytO768Jep3JSGkMnkoA//2Q=="}}}]}},{"type":"media","cover":false,"hidden":false,"anchor":"","data":{"items":[{"title":"","image":{"type":"image","data":{"uuid":"b1c59207-2f3c-5d67-bbd1-8a09dcfe8f88","width":625,"height":333,"size":81800,"type":"png","color":"2f2f37","hash":"","external_service":[],"base64preview":"/9j/4AAQSkZJRgABAQIAOwA7AAD/2wBDAAMCAgICAgMCAgIDAwMDBAYEBAQEBAgGBgUGCQgKCgkICQkKDA8MCgsOCwkJDRENDg8QEBEQCgwSExIQEw8QEBD/2wBDAQMDAwQDBAgEBAgQCwkLEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBD/wAARCAAKAAoDAREAAhEBAxEB/8QAFgABAQEAAAAAAAAAAAAAAAAABQQI/8QAIRAAAgAGAQUAAAAAAAAAAAAAAQIAAwQFESESBhMVVJL/xAAXAQEAAwAAAAAAAAAAAAAAAAAAAQID/8QAFBEBAAAAAAAAAAAAAAAAAAAAAP/aAAwDAQACEQMRAD8Ax/d6uvWqbvXifg7XkWOo2UF+SuHuTvswFvUjMaxULHiEUgZ1mIBMB//Z"}}}]}},{"type":"header","cover":false,"hidden":false,"anchor":"","data":{"style":"h3","text":"3. Безопасность миграций с Flyway"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

✅ 1. Контроль версий

"}},{"type":"list","cover":false,"hidden":false,"anchor":"","data":{"items":["Каждый скрипт имеет уникальную версию (V1, V2, ...).","Flyway не позволяет выполнить одну миграцию дважды (защита от дублирования)."],"type":"UL"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

✅ 2. Идемпотентность (безопасность повторного запуска)

"}},{"type":"list","cover":false,"hidden":false,"anchor":"","data":{"items":["Можно писать SQL так, чтобы миграция не ломалась при повторном запуске:"],"type":"UL"}},{"type":"code","cover":false,"hidden":false,"anchor":"","data":{"text":"-- V3__Add_index_username.sql\nCREATE INDEX IF NOT EXISTS idx_username ON users(username);","lang":""}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

✅ 3. Откат изменений (через откатные миграции)

"}},{"type":"list","cover":false,"hidden":false,"anchor":"","data":{"items":["Flyway поддерживает откатные миграции (U__...sql):"],"type":"UL"}},{"type":"code","cover":false,"hidden":false,"anchor":"","data":{"text":"-- U2__Drop_email_column.sql\nALTER TABLE users DROP COLUMN email;","lang":""}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

✅ 4. Защита от человеческих ошибок

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

Ручные sql скрипты могут:

"}},{"type":"list","cover":false,"hidden":false,"anchor":"","data":{"items":["Забыть применить.","Запустить в неправильном порядке.","Сломать БД из-за опечатки."],"type":"UL"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Flyway гарантирует:

"}},{"type":"list","cover":false,"hidden":false,"anchor":"","data":{"items":["Все миграции выполняются последовательно.","Нельзя пропустить миграцию.","Легко отследить историю изменений."],"type":"UL"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

✅ 5. Интеграция с CI/CD

"}},{"type":"list","cover":false,"hidden":false,"anchor":"","data":{"items":["Flyway можно встроить в автоматические пайплайны (GitHub Actions, Jenkins).","Перед деплоем можно тестировать миграции на staging-БД."],"type":"UL"}},{"type":"header","cover":false,"hidden":false,"anchor":"","data":{"style":"h3","text":"Когда использовать Flyway?"}},{"type":"list","cover":false,"hidden":false,"anchor":"","data":{"items":["В проектах с частыми изменениями структуры БД.","Для командной разработки (чтобы у всех была одинаковая схема БД).","Если важна безопасность и предсказуемость миграций."],"type":"UL"}}],"summaryContent":null,"isExistSummaryContent":false,"warningFromEditor":null,"warningFromEditorTitle":null,"counters":{"comments":0,"favorites":0,"reposts":0,"views":300,"hits":4508,"reads":null,"online":0},"dateFavorite":0,"hitsCount":4508,"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":"Миграции БД с Flyway, безопасность миграций, настройка Flyway в Spring Boot, автоматизация обновлений, контроль версий изменений, примеры миграций.","url":"https://vc.ru/dev/1916577-migratsii-bd-s-flyway-v-java-bezopasnost-i-nastroyka-v-spring-boot","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":1764922259,"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":1}],"reactionId":0},"isNews":false,"source":null,"clusters":[],"donations":{"amount":0,"isDonated":false},"commentsSeenCount":null}},{"type":"entry","data":{"id":1872632,"customUri":null,"subsiteId":2776479,"title":"Монорепозиторий для самых маленьких: как разделить бэкенд и фронтенд для удобного управления проектом","date":1742320473,"dateModified":1742383646,"blocks":[{"type":"text","cover":true,"hidden":false,"anchor":"","data":{"text":"

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

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

Разделение бэкенда и фронтенда — ключевой шаг для создания масштабируемых и поддерживаемых приложений. Но как организовать их так, чтобы не запутаться в десятках файлов и сервисов? В этой статье вы узнаете, как использовать монорепозиторий и Docker для изоляции компонентов, упрощения разработки и быстрого развёртывания.

"}},{"type":"header","cover":false,"hidden":false,"anchor":"","data":{"style":"h2","text":"Зачем разделять бэкенд и фронтенд?"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Изоляция кода: Бэкенд (логика) и фронтенд (интерфейс) работают независимо.

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

Масштабируемость: Легко добавлять новые сервисы или обновлять существующие.

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

Упрощение CI/CD: Каждый компонент можно тестировать и деплоить отдельно.

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

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

"}},{"type":"header","cover":false,"hidden":false,"anchor":"","data":{"style":"h2","text":"Структура монорепозитория"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Монорепозиторий — это единое хранилище для всех частей проекта. Вот как может выглядеть структура для двух приложений (app1 и app2):

"}},{"type":"code","cover":false,"hidden":false,"anchor":"","data":{"text":"mono-repo/\n├── apps/\n│ ├── app1/\n│ │ ├── frontend/ # React, Vue, Angular\n│ │ └── backend/ # Java, Node.js, Python\n│ └── app2/\n│ ├── frontend/\n│ └── backend/\n├── packages/ # Общие компоненты\n└── docker-compose.yml # Управление контейнерами","lang":""}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Преимущества:

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

1)Все компоненты в одном месте.

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

2)Общие библиотеки (например, UI-компоненты) можно использовать в нескольких приложениях.

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

3)Упрощённая настройка зависимостей.

"}},{"type":"header","cover":false,"hidden":false,"anchor":"","data":{"style":"h2","text":"Настраиваем тестовой бэкенд и фронтенд"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Для примера создадим два приложения (app1 и app2), где бэкенд будет возвращать строку, а фронтенд — отображать её. Код будет одинаковым для обоих приложений — разница только в названиях и строках.

"}},{"type":"header","cover":false,"hidden":false,"anchor":"","data":{"style":"h3","text":"1. Бэкенд (Java Spring Boot)"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Для app1

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

Файл: apps/app1/backend/demo/src/main/java/com/example/demo1/HelloController.java

"}},{"type":"code","cover":false,"hidden":false,"anchor":"","data":{"text":"import org.springframework.web.bind.annotation.GetMapping;\nimport org.springframework.web.bind.annotation.RestController;\n\n@RestController\npublic class HelloController {\n @GetMapping(\"/api\")\n public String getMessage() {\n return \"Привет это бекенд от 1\";\n }\n}","lang":""}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Для app2

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

Файл: apps/app2/backend/demo/src/main/java/com/example/demo2/HelloController.java

"}},{"type":"code","cover":false,"hidden":false,"anchor":"","data":{"text":"import org.springframework.web.bind.annotation.GetMapping;\nimport org.springframework.web.bind.annotation.RestController;\n\n@RestController\npublic class HelloController {\n @GetMapping(\"/api\")\n public String getMessage() {\n return \"Hello from Java backend!\";\n }\n}","lang":""}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Что здесь происходит?

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

Каждый бэкенд запускается на своём порту(server.port=8082 - для app1 server.port=8081 - для app2).

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

По пути /api возвращается уникальная строка для каждого приложения.

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

А да точно еще следует сделать WebConfig для портов, а то будет возникать ошибка.

"}},{"type":"code","cover":false,"hidden":false,"anchor":"","data":{"text":"import org.springframework.context.annotation.Configuration;\nimport org.springframework.web.servlet.config.annotation.CorsRegistry;\nimport org.springframework.web.servlet.config.annotation.WebMvcConfigurer;\n\n@Configuration\npublic class WebConfig implements WebMvcConfigurer {\n @Override\n public void addCorsMappings(CorsRegistry registry) {\n registry.addMapping(\"/api/**\")\n .allowedOrigins(\"http://localhost:3002\")\n .allowedMethods(\"GET\", \"POST\", \"PUT\", \"DELETE\", \"OPTIONS\");\n }\n}","lang":""}},{"type":"header","cover":false,"hidden":false,"anchor":"","data":{"style":"h3","text":"2. Фронтенд (React)"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Для app1

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

Файл: apps/app1/frontend/demofront1/src/App.tsx

"}},{"type":"code","cover":false,"hidden":false,"anchor":"","data":{"text":"import React, { useEffect, useState } from 'react';\nimport '@mantine/core/styles.css';\nimport { MantineProvider, Container, Title, Text } from '@mantine/core';\n\nfunction App() {\n const [message, setMessage] = useState('');\n\n useEffect(() => {\n fetch('/api')\n .then(res => res.text())\n .then(data => setMessage(data));\n }, []);\n\n return (\n \n \n \n app2test Frontend\n \n \n Backend response: {message}\n \n \n \n );\n}","lang":""}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Для app2

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

Файл: apps/app2/frontend/demofront/src/App.tsx

"}},{"type":"code","cover":false,"hidden":false,"anchor":"","data":{"text":"import React, { useEffect, useState } from 'react';\n\nfunction App() {\n const [message, setMessage] = useState('');\n\n useEffect(() => {\n fetch('/api')\n .then(res => res.text())\n .then(data => setMessage(data));\n }, []);\n\n return (\n
\n

app1 Frontend

\n

{message}

\n
\n );\n}\nexport default App;","lang":""}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Что здесь происходит?

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

Фронтенд отправляет запрос к /api и отображает ответ бэкенда.

"}},{"type":"header","cover":false,"hidden":false,"anchor":"","data":{"style":"h2","text":"Настраиваем Docker"}},{"type":"header","cover":false,"hidden":false,"anchor":"","data":{"style":"h3","text":"1. Dockerfile для бэкенда"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Для каждого приложения создайте файл Dockerfile в папке бэкенда:

"}},{"type":"media","cover":false,"hidden":false,"anchor":"","data":{"items":[{"title":"","image":{"type":"image","data":{"uuid":"6a508066-e374-5ed5-bdb8-bc29f6a5312a","width":634,"height":802,"size":61040,"type":"png","color":"2e2e35","hash":"","external_service":[],"base64preview":"/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAMCAgICAgMCAgIDAwMDBAYEBAQEBAgGBgUGCQgKCgkICQkKDA8MCgsOCwkJDRENDg8QEBEQCgwSExIQEw8QEBD/2wBDAQMDAwQDBAgEBAgQCwkLEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBD/wAARCAAKAAoDAREAAhEBAxEB/8QAFwAAAwEAAAAAAAAAAAAAAAAAAgMECP/EACIQAAIABQMFAAAAAAAAAAAAAAECAAMEERMjMXEzY5LB0f/EABcBAQEBAQAAAAAAAAAAAAAAAAIDAAH/xAAdEQACAgEFAAAAAAAAAAAAAAAAARESAgMhMVFh/9oADAMBAAIRAxEAPwDGUwUq0MrPKZpri6Nk2W/EK2/BTJRppT2ycYLDTbzHyFbwlAVZ0aU9v3CCJGw4jhj/2Q=="}}}]}},{"type":"code","cover":false,"hidden":false,"anchor":"","data":{"text":"# Используем базовый образ с Maven\nFROM maven:3.8-openjdk-17 AS build\n# Устанавливаем рабочую директорию\nWORKDIR /app\n# Копируем pom.xml и устанавливаем зависимости\nCOPY pom.xml .\nRUN mvn dependency:go-offline\n# Копируем исходный код\nCOPY src ./src\n# Собираем проект\nRUN mvn package -DskipTests\n# Используем базовый образ с Java для запуска\nFROM openjdk:17\n# Устанавливаем рабочую директорию\nWORKDIR /app\n# Копируем собранный JAR-файл\nCOPY --from=build /app/target/*.jar app.jar\n# Открываем порт 8082\nEXPOSE 8082 # Для app1 используйте 8082, для app2 — 8081\n# Команда для запуска приложения\nCMD [\"java\", \"-jar\", \"app.jar\"]","lang":""}},{"type":"header","cover":false,"hidden":false,"anchor":"","data":{"style":"h3","text":"2. Dockerfile для фронтенда"}},{"type":"code","cover":false,"hidden":false,"anchor":"","data":{"text":"# Используем базовый образ Node.js для сборки\nFROM node:18 AS build\n\n# Устанавливаем рабочую директорию\nWORKDIR /app\n\n# Копируем package.json и package-lock.json\nCOPY package*.json ./\n\n# Устанавливаем зависимости\nRUN npm install\n\n# Копируем исходный код\nCOPY . .\n\n# Собираем проект\nRUN npm run build\n\n# Используем базовый образ Nginx для запуска\nFROM nginx:alpine\n\n# Копируем собранные файлы фронтенда\nCOPY --from=build /app/build /usr/share/nginx/html\n\n# Копируем конфигурацию Nginx\nCOPY nginx.conf /etc/nginx/conf.d/default.conf\n\n# Открываем порт 80 \nEXPOSE 80 \n\n\n# Команда для запуска Nginx\nCMD [\"nginx\", \"-g\", \"daemon off;\"]","lang":""}},{"type":"header","cover":false,"hidden":false,"anchor":"","data":{"style":"h3","text":"3. Настройка Nginx (nginx.conf)"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Для каждого фронтенда создайте файл nginx.conf:

"}},{"type":"code","cover":false,"hidden":false,"anchor":"","data":{"text":"server {\n listen 80;\n server_name localhost;\n\n location / {\n root /usr/share/nginx/html;\n try_files $uri $uri/ /index.html;\n }\n\n # Проксирование API-запросов к бэкенду\n location /api {\n proxy_pass http://app1test-backend:8082;\n proxy_set_header Host $host;\n proxy_set_header X-Real-IP $remote_addr;\n proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n proxy_set_header X-Forwarded-Proto $scheme;\n }\n}\n # Для app2:\n # location /api {\n # proxy_pass http://app2-backend:8081;\n # proxy_set_header Host $host;\n # proxy_set_header X-Real-IP $remote_addr;\n # proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;\n # proxy_set_header X-Forwarded-Proto $scheme;\n # }\n}","lang":""}},{"type":"header","cover":false,"hidden":false,"anchor":"","data":{"style":"h2","text":"Связываем всё через docker-compose.yml"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Чтобы связать все компоненты вашего приложения с помощью Docker Compose, вам нужно создать файл docker-compose.yml. Этот файл обычно размещается в mono-repo/.

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

Файл: docker-compose.yml

"}},{"type":"code","cover":false,"hidden":false,"anchor":"","data":{"text":"services:\n app2test-backend:\n build: ./apps/app2test/backend/demo\n ports:\n - \"8081:8081\"\n networks:\n - app-network\n\n app2test-frontend:\n build: ./apps/app2test/frontend/demofront\n ports:\n - \"3000:80\"\n depends_on:\n - app2test-backend\n networks:\n - app-network\n\n app1test-backend:\n build: ./apps/app1test/backend/demo1\n ports:\n - \"8082:8082\"\n networks:\n - app-network\n\n app1test-frontend:\n build: ./apps/app1test/frontend/demofront1\n ports:\n - \"3002:80\"\n depends_on:\n - app1test-backend\n networks:\n - app-network\n\nnetworks:\n app-network:\n driver: bridge","lang":""}},{"type":"header","cover":false,"hidden":false,"anchor":"","data":{"style":"h3","text":"Как это работает?"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Каждое приложение работает в своей «паре» (бэкенд + фронтенд).

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

Сервисы общаются через общую сеть app-network.

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

Фронтенд app1 доступен на http://localhost:3002 , app2 — на http://localhost:3000.

"}},{"type":"header","cover":false,"hidden":false,"anchor":"","data":{"style":"h3","text":"Запускаем проект"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Перейдите в корневую папку (mono-repo).

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

Выполните:

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

docker-compose up --build

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

Проверьте:

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

app1: http://localhost:3002 → Должно отобразиться:

"}},{"type":"media","cover":false,"hidden":false,"anchor":"","data":{"items":[{"title":"","image":{"type":"image","data":{"uuid":"3d321be3-6ce2-5c12-ba4a-71a808c6dfa3","width":842,"height":406,"size":81436,"type":"png","color":"20272a","hash":"","external_service":[],"base64preview":"/9j/4AAQSkZJRgABAQIAOwA7AAD/2wBDAAMCAgICAgMCAgIDAwMDBAYEBAQEBAgGBgUGCQgKCgkICQkKDA8MCgsOCwkJDRENDg8QEBEQCgwSExIQEw8QEBD/2wBDAQMDAwQDBAgEBAgQCwkLEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBD/wAARCAAKAAoDAREAAhEBAxEB/8QAFwAAAwEAAAAAAAAAAAAAAAAAAAYHCP/EACMQAAEDAwIHAAAAAAAAAAAAAAECAwQABQcGEQgTISQyQWH/xAAWAQEBAQAAAAAAAAAAAAAAAAAAAgH/xAAXEQEBAQEAAAAAAAAAAAAAAAAAEgER/9oADAMBAAIRAxEAPwDbOOuGrE2hLo/JsOKrNZVyG+WqUw0pxSgD47FsbD7v6q7TOKiMeWEAARYwA6Dt00tvDTUNFB//2Q=="}}}]}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

app2: http://localhost:3000 → Должно отобразиться:

"}},{"type":"media","cover":false,"hidden":false,"anchor":"","data":{"items":[{"title":"","image":{"type":"image","data":{"uuid":"bd3f9c8d-64e5-58d8-867b-0bc778d3fb13","width":803,"height":386,"size":78977,"type":"png","color":"20282c","hash":"","external_service":[],"base64preview":"/9j/4AAQSkZJRgABAQIAOwA7AAD/2wBDAAMCAgICAgMCAgIDAwMDBAYEBAQEBAgGBgUGCQgKCgkICQkKDA8MCgsOCwkJDRENDg8QEBEQCgwSExIQEw8QEBD/2wBDAQMDAwQDBAgEBAgQCwkLEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBD/wAARCAAKAAoDAREAAhEBAxEB/8QAFwAAAwEAAAAAAAAAAAAAAAAAAAUHCP/EACIQAAECBQQDAAAAAAAAAAAAAAECBAADBgcRBQgSIRMxkv/EABUBAQEAAAAAAAAAAAAAAAAAAAAC/8QAFxEBAQEBAAAAAAAAAAAAAAAAABIBMf/aAAwDAQACEQMRAD8A3prFnKIuU2TotxKOZP2kiaHDZDtKJo8mCMjhxI6J9xdpnN6SK2P7ZlqK1Wlp0lRySWquz9wsnF6iFCA//9k="}}}]}},{"type":"header","cover":false,"hidden":false,"anchor":"","data":{"style":"h2","text":"Как это влияет на безопасность"}},{"type":"list","cover":false,"hidden":false,"anchor":"","data":{"items":["

Изоляция контейнеров Каждый сервис работает в своей «песочнице». Даже если злоумышленник взломает фронтенд, он не сможет напрямую получить доступ к данным бэкенда.

","

Минимальные права Nginx во фронтенде имеет доступ только к статическим файлам, а бэкенд не имеет доступа к файловой системе фронтенда.

","

Сетевые ограничения Сервисы общаются через внутреннюю сеть Docker, которая недоступна из внешнего мира. Порт бэкенда (8081/8082) открыт только для фронтенда.

","

Лёгкое обновление Если в одном из компонентов обнаружена уязвимость, можно быстро пересобрать только его контейнер, не затрагивая остальные части системы.

"],"type":"OL"}},{"type":"header","cover":false,"hidden":false,"anchor":"","data":{"style":"h2","text":"Итог"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Используя монорепозиторий и Docker, вы:

"}},{"type":"list","cover":false,"hidden":false,"anchor":"","data":{"items":["

Упрощаете разработку — все компоненты под рукой.

","

Ускоряете деплой — одна команда запускает весь проект.

","

Повышаете безопасность — изоляция и минимальные права сводят риски к минимуму.

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

Это как конструктор: каждый модуль на своём месте, а Docker — инструкция по сборке. Начните с малого, и вы быстро освоите этот подход!

"}}],"summaryContent":null,"isExistSummaryContent":false,"warningFromEditor":null,"warningFromEditorTitle":null,"counters":{"comments":0,"favorites":0,"reposts":0,"views":342,"hits":513,"reads":null,"online":0},"dateFavorite":0,"hitsCount":513,"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":null,"url":"https://vc.ru/dev/1872632-monorepozitorii-dlya-samyh-malenkih-kak-razdelit-bekend-i-frontend-dlya-udobnogo-upravleniya-proektom","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":1764922259,"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":1}],"reactionId":0},"isNews":false,"source":null,"clusters":[],"donations":{"amount":0,"isDonated":false},"commentsSeenCount":null}},{"type":"entry","data":{"id":1840960,"customUri":null,"subsiteId":2776479,"title":"Использование основ Mantine в TypeScript для создания удобного React-приложения","date":1740854490,"dateModified":1740854490,"blocks":[{"type":"text","cover":true,"hidden":false,"anchor":"","data":{"text":"

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

"}},{"type":"header","cover":false,"hidden":false,"anchor":"","data":{"style":"h2","text":"Ведение"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

В современной веб-разработке важно не только создавать функциональные приложения, но и обеспечивать их удобство и привлекательный внешний вид. Библиотека Mantine

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

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

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

В этой статье мы рассмотрим, как можно обернуть проект в Mantine, а также как использовать его компоненты для создания простейших кнопок и таблиц. В качестве примера будет использовано простое React-приложение с маршрутизацией.

"}},{"type":"header","cover":false,"hidden":false,"anchor":"","data":{"style":"h2","text":"Обертывание проекта в Mantine"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Для начала работы с Mantine необходимо установить библиотеку и ее зависимости. Вы можете сделать это с помощью npm или yarn:

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

npm install @mantine/core @mantine/hooks

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

или

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

yarn add @mantine/core @mantine/hooks

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

После установки библиотеки, мы можем обернуть наше приложение в MantineProvider, который предоставляет доступ ко всем компонентам и темам Mantine. В нашем примере это уже сделано в файле App.tsx:

"}},{"type":"code","cover":false,"hidden":false,"anchor":"","data":{"text":"import React from 'react';\nimport { BrowserRouter as Router, Route, Routes } from 'react-router-dom';\nimport Home from './components/Home';\nimport Login from './components/Login';\nimport { MantineProvider } from '@mantine/core';\nimport '@mantine/core/styles.css';\n\nconst App: React.FC = () => {\n return (\n \n \n \n } />\n } />\n \n \n \n );\n};\nexport default App;","lang":""}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

MantineProvider является корневым компонентом, который обеспечивает доступ к темам и стилям Mantine во всем приложении.

"}},{"type":"header","cover":false,"hidden":false,"anchor":"","data":{"style":"h2","text":"Импорт стилей Mantine"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Для того чтобы стили компонентов Mantine корректно применялись, необходимо импортировать глобальные стили библиотеки. Это делается с помощью строки:

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

import '@mantine/core/styles.css';

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

Этот импорт подключает базовые стили, которые необходимы для корректного отображения компонентов Mantine. Без этого шага компоненты могут выглядеть неправильно или не соответствовать ожидаемому дизайну. В нашем примере это уже сделано в файле App.tsx.

"}},{"type":"header","cover":false,"hidden":false,"anchor":"","data":{"style":"h2","text":"Создание простых кнопок с использованием Mantine"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

(не знаю почему именно кнопки, просто для примера я так захотел)

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

Mantine предоставляет компонент Button, который можно легко настроить под различные нужды. В версии Mantine 7 компоненты стали более модульными и гибкими, что позволяет разработчикам быстро создавать интерфейсы с минимальными усилиями.

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

Для работы с кнопками необходимо импортировать компонент Button из @mantine/core:

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

import { Button } from '@mantine/core';

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

Давайте рассмотрим несколько вариантов использования кнопок в Mantine:

"}},{"type":"header","cover":false,"hidden":false,"anchor":"","data":{"style":"h3","text":"1. Базовая кнопка"}},{"type":"code","cover":false,"hidden":false,"anchor":"","data":{"text":"","lang":""}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Это самая простая кнопка, которая отображает текст \"Нажми меня\". По умолчанию кнопка имеет стиль, соответствующий текущей теме Mantine.

"}},{"type":"header","cover":false,"hidden":false,"anchor":"","data":{"style":"h3","text":"2. Кнопка с измененным размером"}},{"type":"code","cover":false,"hidden":false,"anchor":"","data":{"text":"","lang":""}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Здесь мы используем проп size, чтобы изменить размер кнопки. Mantine поддерживает несколько размеров: xs, sm, md, lg, xl.

"}},{"type":"header","cover":false,"hidden":false,"anchor":"","data":{"style":"h3","text":"3. Кнопка с иконкой"}},{"type":"code","cover":false,"hidden":false,"anchor":"","data":{"text":"import { IconLogin } from '@tabler/icons-react';\n\n","lang":""}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

В этом примере мы добавляем иконку слева от текста кнопки. Для этого используется проп leftSection. Иконка импортируется из библиотеки @tabler/icons-react, которая часто используется вместе с Mantine.

"}},{"type":"header","cover":false,"hidden":false,"anchor":"","data":{"style":"h3","text":"4. Кнопка с градиентом"}},{"type":"code","cover":false,"hidden":false,"anchor":"","data":{"text":"","lang":""}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Здесь мы используем проп variant со значением gradient, чтобы создать кнопку с градиентным фоном. Проп gradient позволяет настроить цвета и угол градиента.

"}},{"type":"header","cover":false,"hidden":false,"anchor":"","data":{"style":"h2","text":"Пример использования кнопок в компоненте Home"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Вернемся к нашему компоненту Home, где мы используем кнопки:

"}},{"type":"code","cover":false,"hidden":false,"anchor":"","data":{"text":"import React from 'react';\nimport { Button, Title, Container } from '@mantine/core';\nimport { useNavigate } from 'react-router-dom';\nimport { IconLogin } from '@tabler/icons-react';\n\nconst Home: React.FC = () => {\n const navigate = useNavigate();\n\n const handleLogin = () => {\n navigate('/login');\n };\n\n return (\n \n Добро пожаловать!\n \n \n \n \n \n );\n};\nexport default Home;","lang":""}},{"type":"media","cover":false,"hidden":false,"anchor":"","data":{"items":[{"title":"","image":{"type":"image","data":{"uuid":"73987813-4fed-537d-b89e-1f561defc24c","width":742,"height":603,"size":69552,"type":"png","color":"2590df","hash":"","external_service":[],"base64preview":"/9j/4AAQSkZJRgABAQIAOwA7AAD/2wBDAAMCAgICAgMCAgIDAwMDBAYEBAQEBAgGBgUGCQgKCgkICQkKDA8MCgsOCwkJDRENDg8QEBEQCgwSExIQEw8QEBD/2wBDAQMDAwQDBAgEBAgQCwkLEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBD/wAARCAAKAAoDAREAAhEBAxEB/8QAFwAAAwEAAAAAAAAAAAAAAAAAAQMHCP/EACMQAAIBBAEDBQAAAAAAAAAAAAECAwAEBRESBgchIjFRcYH/xAAVAQEBAAAAAAAAAAAAAAAAAAAABv/EACERAAEEAQMFAAAAAAAAAAAAAAEAAwQRIQIFEkFhcYGx/9oADAMBAAIRAxEAPwDd3X93n8X3GxGJxOVWGyvXV5YZMrMnMs3q5jyVB9lCEflUm3Q4TsBx18HkLo18yB5tTW5z5Mee0y3qA0msE9+uD6pV6OygjRY1MulAUblcnx8knZqbVKnFVJ2VG/qiUjRF/9k="}}}]}},{"type":"header","cover":false,"hidden":false,"anchor":"","data":{"style":"h2","text":"Удобство использования Mantine"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

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

"}},{"type":"link","cover":false,"hidden":false,"anchor":"","data":{"link":{"type":"link","data":{"url":"https://api.vc.ru/v2.8/redirect?to=https%3A%2F%2Fmantine.dev&postId=1840960","title":"Mantine","description":"","image":{"type":"image","data":{"uuid":"https://leonardo.osnova.io/ico/mantine.dev","width":0,"height":0,"size":0,"type":"jpg","color":"","hash":"","external_service":[]}},"v":1,"hostname":"mantine.dev"}}}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

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

"}},{"type":"header","cover":false,"hidden":false,"anchor":"","data":{"style":"h2","text":"Заключение"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Использование Mantine в сочетании с TypeScript позволяет создавать современные, типобезопасные и легко поддерживаемые React-приложения. Библиотека предоставляет мощные инструменты для создания пользовательских интерфейсов, которые выглядят привлекательно и работают эффективно. В этой статье мы рассмотрели основы работы с Mantine, включая обертывание приложения в MantineProvider, создание простых кнопок и использование глобальных стилей. Вы же можете экспериментировать!!!

"}}],"summaryContent":null,"isExistSummaryContent":false,"warningFromEditor":null,"warningFromEditorTitle":null,"counters":{"comments":0,"favorites":0,"reposts":0,"views":396,"hits":369,"reads":null,"online":0},"dateFavorite":0,"hitsCount":369,"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":null,"url":"https://vc.ru/dev/1840960-ispolzovanie-osnov-mantine-v-typescript-dlya-sozdaniya-udobnogo-react-prilozheniya","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":1764922259,"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":2,"count":1}],"reactionId":0},"isNews":false,"source":null,"clusters":[],"donations":{"amount":0,"isDonated":false},"commentsSeenCount":null}},{"type":"entry","data":{"id":1803773,"customUri":null,"subsiteId":2776479,"title":"Защита токенов с помощью AES-CBC: просто о важном в Java","date":1739197945,"dateModified":1739197945,"blocks":[{"type":"text","cover":true,"hidden":false,"anchor":"","data":{"text":"

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

"}},{"type":"header","cover":false,"hidden":false,"anchor":"","data":{"style":"h2","text":"Введение: Зачем это нужно?"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Токены — это цифровые ключи, которые подтверждают вашу личность в приложениях. Как и настоящие ключи, их нужно защищать. Представьте, что злоумышленник украл ваш токен — он получит полный доступ к аккаунту.

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

AES (Advanced Encryption Standard) — это симметричный алгоритм шифрования, который использует один и тот же ключ для шифрования и дешифрования данных. Он работает с блоками фиксированного размера (128 бит) и поддерживает ключи длиной 128, 192 или 256 бит.

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

CBC (Cipher Block Chaining) — это режим работы AES, который добавляет дополнительную защиту. Каждый блок данных перед шифрованием \"смешивается\" с результатом шифрования предыдущего блока. Это делает шифр более устойчивым к атакам.

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

Зачем шифровать токены?

"}},{"type":"list","cover":false,"hidden":false,"anchor":"","data":{"items":["Защита от перехвата в куках","Предотвращение подделки токенов","Соблюдение стандартов безопасности (например, GDPR)"],"type":"UL"}},{"type":"header","cover":false,"hidden":false,"anchor":"","data":{"style":"h2","text":"Внедряем шифрование в приложение"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Рассмотрим, как интегрировать шифрование токенов с использованием AES в режиме CBC в ваше Spring Boot приложение.

"}},{"type":"header","cover":false,"hidden":false,"anchor":"","data":{"style":"h3","text":"1. Создаем \"шифровальщик\" (AesEncryptionUtil)"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Этот класс — наш цифровой сейф:

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

Сначала создадим класс AesEncryptionUtil, который будет содержать методы для шифрования и дешифрования данных с использованием AES/CBC/PKCS5Padding.

"}},{"type":"code","cover":false,"hidden":false,"anchor":"","data":{"text":"package com.example.demo.security.utils;\n\nimport javax.crypto.Cipher;\nimport javax.crypto.SecretKey;\nimport javax.crypto.spec.IvParameterSpec;\nimport javax.crypto.spec.SecretKeySpec;\nimport java.nio.charset.StandardCharsets;\nimport java.security.SecureRandom;\nimport java.util.Base64;\n\npublic class AesEncryptionUtil {\n\n private static final String AES_ALGORITHM = \"AES/CBC/PKCS5Padding\";\n private static final int IV_SIZE = 16;\n\n public static String encrypt(String data, String secretKey) throws Exception {\n if (secretKey == null || secretKey.length() < 16) {\n throw new IllegalArgumentException(\"AES_SECRET_KEY must be at least 16 characters long.\");\n }\n\n // Генерация случайного IV\n byte[] iv = new byte[IV_SIZE];\n SecureRandom random = new SecureRandom();\n random.nextBytes(iv);\n IvParameterSpec ivSpec = new IvParameterSpec(iv);\n\n // Создание ключа\n SecretKey key = new SecretKeySpec(secretKey.getBytes(StandardCharsets.UTF_8), \"AES\");\n\n // Логирование процесса шифрования\n System.out.println(\"Encrypting data with AES key: \" + secretKey);\n\n // Шифрование\n Cipher cipher = Cipher.getInstance(AES_ALGORITHM);\n cipher.init(Cipher.ENCRYPT_MODE, key, ivSpec);\n byte[] encryptedBytes = cipher.doFinal(data.getBytes(StandardCharsets.UTF_8));\n\n // Объединение IV и зашифрованных данных\n byte[] combined = new byte[iv.length + encryptedBytes.length];\n System.arraycopy(iv, 0, combined, 0, iv.length);\n System.arraycopy(encryptedBytes, 0, combined, iv.length, encryptedBytes.length);\n\n // Логирование результата\n System.out.println(\"Encryption successful. Encrypted data size: \" + combined.length);\n\n return Base64.getEncoder().encodeToString(combined);\n }\n\n public static String decrypt(String encryptedData, String secretKey) throws Exception {\n if (secretKey == null || secretKey.length() < 16) {\n throw new IllegalArgumentException(\"AES_SECRET_KEY must be at least 16 characters long.\");\n }\n\n // Декодирование Base64\n byte[] combined = Base64.getDecoder().decode(encryptedData);\n\n // Извлечение IV и зашифрованных данных\n byte[] iv = new byte[IV_SIZE];\n byte[] encryptedBytes = new byte[combined.length - IV_SIZE];\n System.arraycopy(combined, 0, iv, 0, IV_SIZE);\n System.arraycopy(combined, IV_SIZE, encryptedBytes, 0, encryptedBytes.length);\n\n // Создание ключа\n SecretKey key = new SecretKeySpec(secretKey.getBytes(StandardCharsets.UTF_8), \"AES\");\n\n // Дешифрование\n Cipher cipher = Cipher.getInstance(AES_ALGORITHM);\n cipher.init(Cipher.DECRYPT_MODE, key, new IvParameterSpec(iv));\n byte[] decryptedBytes = cipher.doFinal(encryptedBytes);\n\n // Логирование результата дешифрования\n System.out.println(\"Decryption successful.\");\n\n return new String(decryptedBytes, StandardCharsets.UTF_8);\n }\n}","lang":""}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Представь, что у тебя есть секретное письмо, которое нужно спрятать в сейф.

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

Класс AesEncryptionUtil — это твой робот-шифровальщик, который умеет делать две вещи:

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

Запираем письмо в сейф (шифрование)

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

Шаг 1: Проверяем ключ

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

Робот говорит: «Дайте мне секретный ключ длиной минимум 16 символов!»

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

Вместо того чтобы передавать ключ в метод, получаем его из переменной окружения AES_SECRET_KEY

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

Про файл .env я рассказывал в одной из своих статей

"}},{"type":"osnovaEmbed","cover":false,"hidden":false,"anchor":"","data":{"osnovaEmbed":{"type":"osnovaEmbed","data":{"original_id":1739633,"isNotAvailable":false,"title":"Файл .env и причем тут безопасность","description":"Салимжанов Р.Д.","isEditorial":false,"image":null,"url":"https://vc.ru/dev/1739633-fail-env-i-prichem-tut-bezopasnost","blocks":[{"type":"text","cover":true,"hidden":false,"anchor":"","data":{"text":"

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

"}}],"date":1735911262,"author":{"id":2776479,"name":"Коротко об IT и безопасности","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="}}},"subsite":{"id":235819,"name":"Разработка","avatar":{"type":"image","data":{"uuid":"fef5b5fb-e488-5b7f-8445-e3a26a910b44","width":1200,"height":1200,"size":7757,"type":"png","color":"343434","hash":"04042b2b1c1000","external_service":[]}}},"likes":0,"comments":1,"isBlur":false,"warningFromEditor":null,"warningFromEditorTitle":null}}}},{"type":"media","cover":false,"hidden":false,"anchor":"","data":{"items":[{"title":"","image":{"type":"image","data":{"uuid":"bea3d2a1-2718-5823-bac4-5df5f8b95e05","width":581,"height":164,"size":19794,"type":"png","color":"1e1f26","hash":"","external_service":[],"base64preview":"/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAMCAgICAgMCAgIDAwMDBAYEBAQEBAgGBgUGCQgKCgkICQkKDA8MCgsOCwkJDRENDg8QEBEQCgwSExIQEw8QEBD/2wBDAQMDAwQDBAgEBAgQCwkLEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBD/wAARCAAKAAoDAREAAhEBAxEB/8QAFgABAQEAAAAAAAAAAAAAAAAAAAUI/8QAIRAAAQIEBwAAAAAAAAAAAAAAAAECBAURIRQjJTE1YaP/xAAXAQEBAQEAAAAAAAAAAAAAAAAAAgED/8QAGhEBAAMAAwAAAAAAAAAAAAAAAAIRIQEyUf/aAAwDAQACEQMRAD8AwbgpTbV90quS63R0vlEs66LBSqvL+LjdTcvE4KAP/9k="}}}]}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Шаг 2: Генерируем «соль» (IV)

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

Робот берет 16 случайных кубиков (IV — Initialization Vector).

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

Это как посыпать письмо солью перед тем, как его запечатать — так сейф будет уникальным.

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

Шаг 3: Создаем замок (ключ)

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

Робот превращает ваш ключ в специальный «замок» для сейфа.

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

Шаг 4: Шифруем

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

Письмо (токен) нарезается на кусочки, смешивается с «солью» и запирается в сейф.

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

Робот использует алгоритм AES-CBC — это как сложный пазл, где каждый кусочек зависит от предыдущего.

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

Шаг 5: Упаковываем

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

Сейф (IV + зашифрованные данные) кодируется в Base64 — это как перевод текста на язык роботов, чтобы его можно было безопасно передать.

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

Для расшифровки процесса мы выполняем обратные шаги: декодируем Base64, извлекаем IV и зашифрованные данные, и затем расшифровываем сообщение с использованием того же секретного ключа.

"}},{"type":"header","cover":false,"hidden":false,"anchor":"","data":{"style":"h3","text":"2. Настраиваем безопасность (SecurityConfig)"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Представьте, что у вас есть здание с разными зонами доступа:

"}},{"type":"list","cover":false,"hidden":false,"anchor":"","data":{"items":["Главный вход открыт для всех.","Вход в комнату администратора (/admin/**) только для пользователей с ролью ROLE_ADMIN.","Вход в пользовательскую зону (/user/**) только для пользователей с ROLE_USER.","Везде нужны пропуска (аутентификация)."],"type":"UL"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Это и делает SecurityFilterChain — управляет доступом пользователей к ресурсам, проверяя их права.

"}},{"type":"code","cover":false,"hidden":false,"anchor":"","data":{"text":"package com.example.demo.config;\n\nimport com.example.demo.security.CustomOAuth2UserService;\nimport com.example.demo.security.utils.AesEncryptionUtil;\nimport jakarta.servlet.http.Cookie;\nimport org.springframework.context.annotation.Bean;\nimport org.springframework.context.annotation.Configuration;\nimport org.springframework.security.config.annotation.web.builders.HttpSecurity;\nimport org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;\nimport org.springframework.security.web.SecurityFilterChain;\n\n\n@Configuration\n@EnableWebSecurity\npublic class SecurityConfig {\n\n private final CustomOAuth2UserService customOAuth2UserService;\n\n public SecurityConfig(CustomOAuth2UserService customOAuth2UserService) {\n this.customOAuth2UserService = customOAuth2UserService;\n System.out.println(\"SecurityConfig initialized\");\n }\n\n @Bean\n public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {\n http\n .authorizeHttpRequests(authorize -> authorize\n .requestMatchers(\"/\", \"/index\").permitAll()\n .requestMatchers(\"/admin/**\").hasAuthority(\"ROLE_ADMIN\")\n .requestMatchers(\"/user/**\").hasAuthority(\"ROLE_USER\")\n .anyRequest().authenticated()\n )\n .oauth2Login(oauth2Login -> oauth2Login\n .userInfoEndpoint(userInfoEndpoint -> userInfoEndpoint\n .oidcUserService(customOAuth2UserService)\n )\n .successHandler((request, response, authentication) -> {\n // Логирование токена перед шифрованием\n String token = authentication.getCredentials().toString();\n System.out.println(\"Token received: \" + token);\n\n String encryptedToken = null;\n try {\n // Логирование секретного ключа\n String secretKey = System.getenv(\"AES_SECRET_KEY\");\n if (secretKey == null) {\n System.out.println(\"AES_SECRET_KEY is not set in environment variables!\");\n } else {\n System.out.println(\"AES_SECRET_KEY found: \" + secretKey);\n }\n\n encryptedToken = AesEncryptionUtil.encrypt(token, secretKey);\n } catch (Exception e) {\n System.out.println(\"Encryption failed: \" + e.getMessage());\n throw new RuntimeException(\"Encryption failed\", e);\n }\n\n // Логирование зашифрованного токена\n System.out.println(\"Encrypted token: \" + encryptedToken);\n\n Cookie cookie = new Cookie(\"encrypted_token\", encryptedToken);\n cookie.setHttpOnly(true);\n cookie.setSecure(true); // Только для HTTPS\n cookie.setPath(\"/\");\n cookie.setMaxAge(7 * 24 * 60 * 60); // 7 дней\n response.addCookie(cookie);\n\n // Логирование успешного завершения\n System.out.println(\"Encrypted token set in cookie.\");\n\n response.sendRedirect(\"/\");\n })\n )\n .logout(logout -> logout\n .deleteCookies(\"encrypted_token\")\n .invalidateHttpSession(true)\n .clearAuthentication(true)\n .logoutSuccessUrl(\"/\")\n )\n .exceptionHandling(exception -> exception\n .accessDeniedPage(\"/access-denied\")\n );\n\n return http.build();\n }\n}","lang":""}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Шаг 1: Получаем токен

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

После успешного входа Spring передает нам токен аутентификации:

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

String token = authentication.getCredentials().toString();

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

Шаг 2: Получаем секретный ключ

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

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

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

String secretKey = System.getenv(\"AES_SECRET_KEY\");

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

if (secretKey == null) {

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

System.out.println(\"AES_SECRET_KEY is not set in environment variables!\");

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

}

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

Шаг 3: Шифруем токен

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

Теперь даже если кто-то украдет cookie, он не сможет расшифровать токен без секретного ключа.

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

String encryptedToken = AesEncryptionUtil.encrypt(token, secretKey);

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

System.out.println(\"Encrypted token: \" + encryptedToken);

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

Шаг 4: Сохраняем в cookie

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

Мы создаем cookie с зашифрованным токеном, которая:

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

1)HttpOnly—браузер не может прочитать её через JavaScript (защита от XSS).

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

2)Secure—отправляется только через HTTPS.

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

3)MaxAge—живёт 7 дней.

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

Cookie cookie = new Cookie(\"encrypted_token\", encryptedToken);

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

cookie.setHttpOnly(true);

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

cookie.setSecure(true); // Только HTTPS

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

cookie.setPath(\"/\");

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

cookie.setMaxAge(7 * 24 * 60 * 60); // 7 дней

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

response.addCookie(cookie);

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

System.out.println(\"Encrypted token set in cookie.\");

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

После этого происходит редирект на главную страницу:

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

response.sendRedirect(\"/\");

"}},{"type":"header","cover":false,"hidden":false,"anchor":"","data":{"style":"h3","text":"Проверка"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

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

"}},{"type":"media","cover":false,"hidden":false,"anchor":"","data":{"items":[{"title":"","image":{"type":"image","data":{"uuid":"d1b4a709-4f06-5eb9-9dbe-26cb5d9993e0","width":869,"height":228,"size":133010,"type":"png","color":"1c1c24","hash":"","external_service":[],"base64preview":"/9j/4AAQSkZJRgABAQIAOwA7AAD/2wBDAAMCAgICAgMCAgIDAwMDBAYEBAQEBAgGBgUGCQgKCgkICQkKDA8MCgsOCwkJDRENDg8QEBEQCgwSExIQEw8QEBD/2wBDAQMDAwQDBAgEBAgQCwkLEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBD/wAARCAAKAAoDAREAAhEBAxEB/8QAFgABAQEAAAAAAAAAAAAAAAAABAUI/8QAJBAAAQMCBAcAAAAAAAAAAAAAAQACAwQRBRIhMRMVFkFhktH/xAAWAQEBAQAAAAAAAAAAAAAAAAAAAgP/xAAUEQEAAAAAAAAAAAAAAAAAAAAA/9oADAMBAAIRAxEAPwDEnKcP4bi6jj1BLSwyX87nf4tUC9P0vaom9EEmukklnzSPc92UauNygOg//9k="}}}]}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Давайте разберем каждую строку:

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

Token received:

"}},{"type":"list","cover":false,"hidden":false,"anchor":"","data":{"items":["

Это сообщение указывает на то, что токен был получен. Однако сам токен не отображается в логах, возможно, по соображениям безопасности.

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

AES_SECRET_KEY found: 1234567890abcdef

"}},{"type":"list","cover":false,"hidden":false,"anchor":"","data":{"items":["

Здесь сообщается, что был найден секретный ключ для шифрования AES. В данном случае ключ имеет значение 1234567890abcdef. Обратите внимание, что использование простых и предсказуемых ключей (как в этом примере) не является безопасной практикой.

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

Encrypting data with AES key: 1234567890abcdef

"}},{"type":"list","cover":false,"hidden":false,"anchor":"","data":{"items":["

Эта строка указывает на то, что данные (в данном случае токен) шифруются с использованием найденного ключа AES.

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

Encryption successful. Encrypted data size: 32

"}},{"type":"list","cover":false,"hidden":false,"anchor":"","data":{"items":["

Сообщение подтверждает, что шифрование прошло успешно, и указывает размер зашифрованных данных (в байтах). В данном случае размер составляет 32 байта.

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

Encrypted token: 4bJ7ok9Vo+/NUwPoxqfBLlev33ne3Gxulfib9szmL4M=

"}},{"type":"list","cover":false,"hidden":false,"anchor":"","data":{"items":["

Это зашифрованный токен, представленный в виде строки, закодированной в Base64. Эта строка может быть использована для передачи или хранения зашифрованного токена.

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

Encrypted token set in cookie.

"}},{"type":"list","cover":false,"hidden":false,"anchor":"","data":{"items":["

В этой строке сообщается, что зашифрованный токен был установлен в cookie. Это означает, что зашифрованные данные будут храниться в cookie браузера пользователя, что позволяет использовать их для аутентификации или авторизации при последующих запросах.

"],"type":"UL"}},{"type":"header","cover":false,"hidden":false,"anchor":"","data":{"style":"h2","text":"Вывод: Это действительно необходимо?"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Почему стоит потерпеть сложности?

"}},{"type":"list","cover":false,"hidden":false,"anchor":"","data":{"items":["Защита от атак: Даже при утечке куки злоумышленник не получит настоящий токен","Соблюдение стандартов: Требуется по PCI DSS, HIPAA и другим регуляторам","Доверие пользователей: Ваши клиенты уверены в безопасности"],"type":"OL"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

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

"}},{"type":"header","cover":false,"hidden":false,"anchor":"","data":{"style":"h2","text":"СПИСОК ИСПОЛЬЗОВАННЫХ ИСТОЧНИКОВ"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

1) Java AES Encryption and Decryption// [электронный ресурс]. URL: https://www.baeldung.com/java-aes-encryption-decryption (дата обращения 08.02.2025).

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

2) AES шифрование и Android клиент // [электронный ресурс]. URL: https://habr.com/ru/companies/rambler_and_co/articles/279835/ (дата обращения 09.02.2025).

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

3) аутентификация при помощи Spring Boot // [электронный ресурс]. URL: https://habr.com/ru/articles/784508/ (дата обращения 08.02.2025).

"}}],"summaryContent":null,"isExistSummaryContent":false,"warningFromEditor":null,"warningFromEditorTitle":null,"counters":{"comments":1,"favorites":0,"reposts":0,"views":461,"hits":503,"reads":null,"online":0},"dateFavorite":0,"hitsCount":503,"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":null,"url":"https://vc.ru/dev/1803773-zashita-tokenov-s-pomoshyu-aes-cbc-prosto-o-vazhnom-v-java","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":1764922259,"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":1},{"id":2,"count":1}],"reactionId":0},"isNews":false,"source":null,"clusters":[],"donations":{"amount":0,"isDonated":false},"commentsSeenCount":null}},{"type":"entry","data":{"id":1784409,"customUri":null,"subsiteId":2776479,"title":"Динамическое управление доступом: Роли пользователей на лету с Spring Boot и Keycloak.","date":1738250325,"dateModified":1738250608,"blocks":[{"type":"text","cover":true,"hidden":false,"anchor":"","data":{"text":"

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

"}},{"type":"header","cover":false,"hidden":false,"anchor":"","data":{"style":"h2","text":"Введение"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

В современном приложении крайне важно организовать безопасный доступ пользователей к различным ресурсам в зависимости от их роли. В этой статье мы рассмотрим, как реализовать ролевую модель доступа, используя Spring Boot, PostgreSQL и Keycloak в качестве сервера авторизации.

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

В предыдущей статье я уже разбирал, как настроить Keycloak и Spring Security на Java. На основе того приложения мы добавим ролевую модель, так чтобы роли пользователей извлекались динамически из базы данных PostgreSQL, а не хранились статически в токенах. Такой подход обеспечит большую гибкость и безопасность, позволяя изменять роли пользователей без необходимости пересоздания токенов.

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

Предыдущая статья:

"}},{"type":"osnovaEmbed","cover":false,"hidden":false,"anchor":"","data":{"osnovaEmbed":{"type":"osnovaEmbed","data":{"original_id":1779404,"isNotAvailable":false,"title":"От Hello World к Secure API: настраиваем Keycloak и Spring Security на Java","description":"Салимжанов Р.Д.","isEditorial":false,"image":null,"url":"https://vc.ru/dev/1779404-ot-hello-world-k-secure-api-nastraivaem-keycloak-i-spring-security-na-java","blocks":[{"type":"text","cover":true,"hidden":false,"anchor":"","data":{"text":"

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

"}}],"date":1738068113,"author":{"id":2776479,"name":"Коротко об IT и безопасности","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="}}},"subsite":{"id":235819,"name":"Разработка","avatar":{"type":"image","data":{"uuid":"fef5b5fb-e488-5b7f-8445-e3a26a910b44","width":1200,"height":1200,"size":7757,"type":"png","color":"343434","hash":"04042b2b1c1000","external_service":[]}}},"likes":0,"comments":0,"isBlur":false,"warningFromEditor":null,"warningFromEditorTitle":null}}}},{"type":"header","cover":false,"hidden":false,"anchor":"","data":{"style":"h2","text":"Настройка Keycloak, базы данных и приложения."}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Итак, я не вносил изменений в файл docker-compose.yml, который мы будем использовать для запуска Keycloak. Как и в прошлый раз, запускаем контейнер с помощью команды docker-compose up . После успешного запуска контейнера вы сможете получить доступ к Keycloak по адресу http://localhost:8080. Далее мы можем продолжить настройку Keycloak в соответствии с требованиями.

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

Если вдруг он уже настроен, вы можете работать в нем, или удалить контейнеры, сети, образы и данные, чтобы очистить окружение. Выбор за вами. Я почистил и настроил заново:

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

Realm name: app_realm

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

Client ID: spring-app

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

Users: test_user

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

Email: test@example.com

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

Password: test123

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

Подробную настройку можно почитать в моей предыдущей статье.

"}},{"type":"header","cover":false,"hidden":false,"anchor":"","data":{"style":"h2","text":"Далее Настройка Mappers (Мапперов токена)."}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Более подробно мы рассмотрим, как настроить мапперы токена в Keycloak для добавления свойства preferred_username в токен доступа. Это необходимо для того, чтобы приложение могло получать и использовать имя пользователя в удобном формате, что упрощает управление доступом и персонализацию пользовательского опыта.

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

1)В клиенте spring-app перейдите на вкладку \"Client Scopes\": Это позволит вам управлять областями доступа, которые определяют, какие данные будут включены в токены для вашего клиента.

"}},{"type":"media","cover":false,"hidden":false,"anchor":"","data":{"items":[{"title":"","image":{"type":"image","data":{"uuid":"f269fb33-8fa4-5ddd-aa07-0d98433eb996","width":974,"height":608,"size":43541,"type":"png","color":"232323","hash":"","external_service":[],"base64preview":"/9j/4AAQSkZJRgABAQIAOwA7AAD/2wBDAAMCAgICAgMCAgIDAwMDBAYEBAQEBAgGBgUGCQgKCgkICQkKDA8MCgsOCwkJDRENDg8QEBEQCgwSExIQEw8QEBD/2wBDAQMDAwQDBAgEBAgQCwkLEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBD/wAARCAAKAAoDAREAAhEBAxEB/8QAFwAAAwEAAAAAAAAAAAAAAAAABAUGCf/EACcQAAIAAwUJAQAAAAAAAAAAAAECAAMHBAYSIXEFFyIyQVFXYZXT/8QAFQEBAQAAAAAAAAAAAAAAAAAAAAL/xAAXEQEBAQEAAAAAAAAAAAAAAAAAAREh/9oADAMBAAIRAxEAPwDQeVRalCLZllUzuhhIGMNsOzMWBHcplnrFSzLsTZeYY7kqQeK7pfFsv5xKlaiIZEolAeBenqAKXlGkB//Z"}}}]}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

2)Выберите \"spring-app-dedicated\" вашего клиента: Выбор конкретной области доступа гарантирует, что изменения будут применены только к нужному клиенту.

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

3)Нажмите \"Add mapper(By configuration)\": Это действие позволит вам создать новый маппер, который будет добавлять необходимые данные в токен.

"}},{"type":"media","cover":false,"hidden":false,"anchor":"","data":{"items":[{"title":"","image":{"type":"image","data":{"uuid":"69c9de66-a58d-5920-8ecd-c0e812ecc5d9","width":974,"height":469,"size":22868,"type":"png","color":"1f2021","hash":"","external_service":[],"base64preview":"/9j/4AAQSkZJRgABAQIAOwA7AAD/2wBDAAMCAgICAgMCAgIDAwMDBAYEBAQEBAgGBgUGCQgKCgkICQkKDA8MCgsOCwkJDRENDg8QEBEQCgwSExIQEw8QEBD/2wBDAQMDAwQDBAgEBAgQCwkLEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBD/wAARCAAKAAoDAREAAhEBAxEB/8QAFwAAAwEAAAAAAAAAAAAAAAAAAAQGCf/EACEQAAEDBAEFAAAAAAAAAAAAAAEDBAcAAgYRYRIVQVHS/8QAFQEBAQAAAAAAAAAAAAAAAAAAAAH/xAAXEQEAAwAAAAAAAAAAAAAAAAAAARFB/9oADAMBAAIRAxEAPwDQdlAUSIKOg9iHBFk+vbYJ4+2tIs9XEg7u35GhxVmsSL0+IFhAgEw5hY47E1+Kir2gKD//2Q=="}}}]}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

4)Выберите \"User Property\": Этот тип маппера позволяет извлекать свойства пользователя из базы данных Keycloak.

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

5) Заполните поля и сохраните:

"}},{"type":"media","cover":false,"hidden":false,"anchor":"","data":{"items":[{"title":"","image":{"type":"image","data":{"uuid":"a6299e39-46c0-5c58-85bb-ac7bb83d6db3","width":974,"height":495,"size":23833,"type":"png","color":"202121","hash":"","external_service":[],"base64preview":"/9j/4AAQSkZJRgABAQIAOwA7AAD/2wBDAAMCAgICAgMCAgIDAwMDBAYEBAQEBAgGBgUGCQgKCgkICQkKDA8MCgsOCwkJDRENDg8QEBEQCgwSExIQEw8QEBD/2wBDAQMDAwQDBAgEBAgQCwkLEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBD/wAARCAAKAAoDAREAAhEBAxEB/8QAFwAAAwEAAAAAAAAAAAAAAAAAAwQGCf/EACQQAAIABAQHAAAAAAAAAAAAAAECAAQFBwMGE3IRFyExV5XS/8QAFgEBAQEAAAAAAAAAAAAAAAAAAAID/8QAGhEBAAEFAAAAAAAAAAAAAAAAAAIDMTKRsf/aAAwDAQACEQMRAD8A0MlrDWUdNRLR5NIJIHGhSp6A7IA/Iay3iDJfoZX4gLanKqy7BQANXE7bzGlXLXEQsajNb//Z"}}}]}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

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

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

Щя покажу, как это работает:

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

Для начала сгенерируем токен.

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

curl -X POST http://localhost:8080/realms/app_realm/protocol/openid-connect/token -H \"Content-Type: application/x-www-form-urlencoded\" -d \"client_id=spring-app\" -d \"client_secret=ВАШ_КЛИЕНТСКИЙ_СЕКРЕТ\" -d \"username=test_user\" -d \"password=test123\" -d \"grant_type=password\"

"}},{"type":"media","cover":false,"hidden":false,"anchor":"","data":{"items":[{"title":"","image":{"type":"image","data":{"uuid":"c723380d-b78f-5106-8291-5cf077b00833","width":974,"height":362,"size":620660,"type":"png","color":"0f0f13","hash":"","external_service":[],"base64preview":"/9j/4AAQSkZJRgABAQIAOwA7AAD/2wBDAAMCAgICAgMCAgIDAwMDBAYEBAQEBAgGBgUGCQgKCgkICQkKDA8MCgsOCwkJDRENDg8QEBEQCgwSExIQEw8QEBD/2wBDAQMDAwQDBAgEBAgQCwkLEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBD/wAARCAAKAAoDAREAAhEBAxEB/8QAFwAAAwEAAAAAAAAAAAAAAAAAAQMFCP/EACEQAAICAQIHAAAAAAAAAAAAAAECAwQAEZIUIjEyM0JS/8QAFAEBAAAAAAAAAAAAAAAAAAAAAP/EABQRAQAAAAAAAAAAAAAAAAAAAAD/2gAMAwEAAhEDEQA/AMQvVryBXeNWK8o6jAPA1fld2BHlsWAq6Tydx9jgKM8+vmfccD//2Q=="}}}]}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Скопируйте access_token из ответа.

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

Вставьте на https://jwt.io

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

Если все правильно, то вы должны увидеть \"preferred_username\": \"test_user\".

"}},{"type":"media","cover":false,"hidden":false,"anchor":"","data":{"items":[{"title":"","image":{"type":"image","data":{"uuid":"f5f145d4-4d9f-5e5a-b858-f8d3e0ab5a4c","width":881,"height":201,"size":41690,"type":"png","color":"f7f2cf","hash":"","external_service":[],"base64preview":"/9j/4AAQSkZJRgABAQIAOwA7AAD/2wBDAAMCAgICAgMCAgIDAwMDBAYEBAQEBAgGBgUGCQgKCgkICQkKDA8MCgsOCwkJDRENDg8QEBEQCgwSExIQEw8QEBD/2wBDAQMDAwQDBAgEBAgQCwkLEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBD/wAARCAAKAAoDAREAAhEBAxEB/8QAFwAAAwEAAAAAAAAAAAAAAAAAAQIFCf/EACQQAAIBAgMJAAAAAAAAAAAAAAECAwAEBQYxERIhIyQ0UnPB/8QAGAEAAgMAAAAAAAAAAAAAAAAAAgYBAwX/xAAjEQACAQIEBwAAAAAAAAAAAAAAAQIDEQQFE1EhMTM0QaHw/9oADAMBAAIRAxEAPwDTfAysdsrIpAWIkDWsHKrvFVL/AHEustCCQq5igKgtZ3G0jj0702PCSXn2FpPcOAdsnr+0qZX3dQCPQgUVgh3RyU08RTE5yvzIcnuf/9k="}}}]}},{"type":"header","cover":false,"hidden":false,"anchor":"","data":{"style":"h3","text":"Подготовка базы данных"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Создадим базу данных для хранения пользователей и их ролей. Схема включает три таблицы:

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

roles — хранит роли (USER, ADMIN, MODERATOR).

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

users — хранит информацию о пользователях (username, email).

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

user_roles — связывает пользователей и их роли.

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

SQL-запросы для создания базы данных

"}},{"type":"code","cover":false,"hidden":false,"anchor":"","data":{"text":"-- 1. Создаем базу данных\nCREATE DATABASE security_roles;\n\n-- 2. Создаем таблицу пользователей\nCREATE TABLE users (\n id SERIAL PRIMARY KEY,\n username VARCHAR(50) UNIQUE NOT NULL,\n email VARCHAR(100) UNIQUE NOT NULL\n);\n\n-- 3. Создаем таблицу ролей\nCREATE TABLE roles (\n id SERIAL PRIMARY KEY,\n name VARCHAR(50) UNIQUE NOT NULL\n);\n\n-- 4. Создаем таблицу связи пользователей и ролей\nCREATE TABLE user_roles (\n user_id INT,\n role_id INT,\n PRIMARY KEY (user_id, role_id),\n FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,\n FOREIGN KEY (role_id) REFERENCES roles(id) ON DELETE CASCADE\n);\n\n-- 5. Добавляем тестовые данные\nINSERT INTO roles (name) VALUES \n('USER'), \n('ADMIN'), \n('MODERATOR');\n\nINSERT INTO users (username, email) VALUES \n('user1', 'user1@example.com'),\n('admin1', 'admin1@example.com');\n\nINSERT INTO user_roles (user_id, role_id) VALUES\n(1, 1), -- user1 имеет роль USER\n(2, 2); -- admin1 имеет роль ADMIN\n\nINSERT INTO users (username, email) VALUES \n('test_user', 'test@example.com');\n\nINSERT INTO user_roles (user_id, role_id) VALUES\n(3, 1), -- test_user имеет роль USER\n(3, 3); -- test_user имеет роль MODERATOR","lang":""}},{"type":"header","cover":false,"hidden":false,"anchor":"","data":{"style":"h2","text":"Конфигурация Spring Boot"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

В файле application.properties укажем параметры подключения к базе данных и Keycloak:

"}},{"type":"code","cover":false,"hidden":false,"anchor":"","data":{"text":"server.port=8081\n\nspring.security.oauth2.resourceserver.jwt.issuer-uri=http://localhost:8080/realms/app_realm\n\n\nspring.datasource.url=jdbc:postgresql://localhost:5432/security_roles\nspring.datasource.username=postgres\nspring.datasource.password=admin\nspring.jpa.hibernate.ddl-auto=validate\nspring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect","lang":""}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Обновление зависимостей в pom.xml:

"}},{"type":"code","cover":false,"hidden":false,"anchor":"","data":{"text":"\n \n org.springframework.boot\n spring-boot-starter-data-jpa\n \n \n org.postgresql\n postgresql\n runtime\n ","lang":""}},{"type":"header","cover":false,"hidden":false,"anchor":"","data":{"style":"h2","text":"Пишем код"}},{"type":"header","cover":false,"hidden":false,"anchor":"","data":{"style":"h3","text":"Создание сущностей и репозитория"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Сущность RoleEntity

"}},{"type":"code","cover":false,"hidden":false,"anchor":"","data":{"text":"@Entity\n@Table(name = \"roles\")\npublic class RoleEntity {\n @Id\n @GeneratedValue(strategy = GenerationType.IDENTITY)\n private Long id;\n\n @Column(nullable = false, unique = true, length = 50)\n private String name;\n\n @ManyToMany(mappedBy = \"roles\", fetch = FetchType.LAZY)\n private Set users = new HashSet<>();\n}","lang":""}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Файл RoleEntity — это как список всех ролей в нашей компании. Например, есть роли \"Администратор\", \"Пользователь\", \"Модератор\". Эти роли — как ярлыки, которые помогают нам понять, что каждый человек может или не может делать. Когда пользователь регистрируется или входит в систему, система смотрит, какая у него роль, чтобы знать, что ему разрешено делать.

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

Сущность UserEntity

"}},{"type":"code","cover":false,"hidden":false,"anchor":"","data":{"text":"@Entity\n@Table(name = \"users\")\npublic class UserEntity {\n @Id\n @GeneratedValue(strategy = GenerationType.IDENTITY)\n private Long id;\n\n @Column(nullable = false, unique = true, length = 50)\n private String username;\n\n @Column(nullable = false, unique = true, length = 100)\n private String email;\n\n @ManyToMany(fetch = FetchType.EAGER)\n @JoinTable(\n name = \"user_roles\",\n joinColumns = @JoinColumn(name = \"user_id\"),\n inverseJoinColumns = @JoinColumn(name = \"role_id\")\n )\n private Set roles = new HashSet<>();\n}","lang":""}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Файл UserEntity — это как список всех сотрудников. Каждый сотрудник — это отдельный пользователь в системе. У каждого пользователя есть ссылка на его роль через RoleId. Если у Анны RoleId = 1, значит, она — Администратор, потому что в таблице ролей под номером 1 написано \"Администратор\".

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

Репозиторий UserRepository

"}},{"type":"code","cover":false,"hidden":false,"anchor":"","data":{"text":"public interface UserRepository extends JpaRepository {\n // Стандартный метод поиска по имени\n Optional findByUsername(String username);\n}","lang":""}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

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

"}},{"type":"header","cover":false,"hidden":false,"anchor":"","data":{"style":"h3","text":"Конфигурация безопасности KeycloakJwtAuthenticationConverter"}},{"type":"code","cover":false,"hidden":false,"anchor":"","data":{"text":"public class KeycloakJwtAuthenticationConverter implements Converter {\n private final JwtGrantedAuthoritiesConverter defaultConverter = new JwtGrantedAuthoritiesConverter();\n private final UserRepository userRepository;\n\n public KeycloakJwtAuthenticationConverter(UserRepository userRepository) {\n this.userRepository = userRepository;\n }\n\n @Override\n public AbstractAuthenticationToken convert(Jwt jwt) {\n Collection authorities = new ArrayList<>(defaultConverter.convert(jwt));\n\n // Получаем username из токена\n String username = jwt.getClaimAsString(\"preferred_username\");\n\n // Ищем пользователя в БД и добавляем роли\n userRepository.findByUsername(username)\n .ifPresent(user ->\n user.getRoles().forEach(role ->\n authorities.add(new SimpleGrantedAuthority(\"ROLE_\" + role.getName()))\n )\n );\n\n return new JwtAuthenticationToken(jwt, authorities);\n }\n}","lang":""}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Когда пользователь присылает токен (например, при входе или доступе к странице), этот файл \"читает\" токен и вытаскивает важную информацию, как имя пользователя и другие данные.

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

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

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

SecurityConfig

"}},{"type":"code","cover":false,"hidden":false,"anchor":"","data":{"text":"@Configuration\n@EnableWebSecurity\npublic class SecurityConfig {\n\n private final UserRepository userRepository;\n\n public SecurityConfig(UserRepository userRepository) {\n this.userRepository = userRepository;\n }\n\n @Bean\n public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {\n http\n .csrf(CsrfConfigurer::disable)\n .authorizeHttpRequests(authorize -> authorize\n .requestMatchers(\"/public/**\").permitAll()\n .requestMatchers(\"/admin\").hasRole(\"ADMIN\")\n .requestMatchers(\"/moderator\").hasRole(\"MODERATOR\")\n .requestMatchers(\"/user\").hasRole(\"USER\")\n .anyRequest().authenticated()\n )\n .oauth2ResourceServer(oauth2 -> oauth2\n .jwt(jwt -> jwt\n .jwtAuthenticationConverter(new KeycloakJwtAuthenticationConverter(userRepository))\n )\n );\n\n return http.build();\n }\n}","lang":""}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Это файл охранник у ворот. Он решает, кто может пройти в систему, а кто нет. Здесь прописаны все правила безопасности.

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

А ну конечно не забудем про TestController

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

Это испытатель системы. Этот файл нужен для проверки работы системы и её функций.

"}},{"type":"code","cover":false,"hidden":false,"anchor":"","data":{"text":"@RestController\npublic class TestController {\n\n @GetMapping(\"/public/hello\")\n public String publicHello() {\n return \"Public access!\";\n }\n\n @GetMapping(\"/user\")\n public String userEndpoint() {\n return \"User area!\";\n }\n\n @GetMapping(\"/admin\")\n public String adminEndpoint() {\n return \"Admin area!\";\n }\n\n @GetMapping(\"/moderator\")\n public String moderatorEndpoint() {\n return \"Moderator area!\";\n }\n\n}","lang":""}},{"type":"header","cover":false,"hidden":false,"anchor":"","data":{"style":"h2","text":"Как всё это работает вместе?"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Когда пользователь входит в систему:

"}},{"type":"list","cover":false,"hidden":false,"anchor":"","data":{"items":["Он присылает токен (как билет).","KeycloakJwtAuthenticationConverter читает токен и передаёт данные системе."],"type":"UL"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Система проверяет пользователя:

"}},{"type":"list","cover":false,"hidden":false,"anchor":"","data":{"items":["SecurityConfig определяет, может ли этот пользователь зайти на нужную страницу.","Например, если это страница для Администраторов, файл проверяет, есть ли у пользователя роль Администратора."],"type":"UL"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Когда система работает с пользователями:

"}},{"type":"list","cover":false,"hidden":false,"anchor":"","data":{"items":["Для добавления или получения данных о пользователе используется UserRepository.","Например, чтобы узнать роль пользователя, система найдёт его через UserRepository и посмотрит, какой у него RoleId."],"type":"UL"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Ну а далее тесты, над пользователем test_user:

"}},{"type":"code","cover":false,"hidden":false,"anchor":"","data":{"text":"# User endpoint (должен работать, так как у test_user есть роль USER)\ncurl -H \"Authorization: Bearer REAL_TOKEN\" http://localhost:8081/user\n\n# Moderator endpoint (должен работать, так как у test_user есть роль MODERATOR)\ncurl -H \"Authorization: Bearer REAL_TOKEN\" http://localhost:8081/moderator\n\n# Admin endpoint (не должен работать, так как прав нет)\ncurl -H \"Authorization: Bearer REAL_TOKEN\" http://localhost:8081/admin","lang":""}},{"type":"media","cover":false,"hidden":false,"anchor":"","data":{"items":[{"title":"","image":{"type":"image","data":{"uuid":"81d0cb24-795f-5eaa-843f-da3e9ed79141","width":974,"height":673,"size":312395,"type":"png","color":"959c95","hash":"","external_service":[],"base64preview":"/9j/4AAQSkZJRgABAQIAOwA7AAD/2wBDAAMCAgICAgMCAgIDAwMDBAYEBAQEBAgGBgUGCQgKCgkICQkKDA8MCgsOCwkJDRENDg8QEBEQCgwSExIQEw8QEBD/2wBDAQMDAwQDBAgEBAgQCwkLEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBD/wAARCAAKAAoDAREAAhEBAxEB/8QAFgABAQEAAAAAAAAAAAAAAAAAAgUI/8QAIhAAAgIBAQkAAAAAAAAAAAAAAQMCBAATERRCUVNUgZKU/8QAFAEBAAAAAAAAAAAAAAAAAAAAAP/EABQRAQAAAAAAAAAAAAAAAAAAAAD/2gAMAwEAAhEDEQA/AMMakxI7Gy8VpYC1W9VnzSwJbLNkNYBYYAJHjPPAO9We4b7nA//Z"}}}]}},{"type":"header","cover":false,"hidden":false,"anchor":"","data":{"style":"h3","text":"Вывод"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

Динамическое управление ролями через базу данных в интеграции с Keycloak обеспечивает гибкость и удобство. Вы можете легко обновлять роли пользователей и изменять их права доступа без необходимости обновлять токены. Такой подход подходит для масштабируемых приложений, где требуется централизованное управление правами.

"}}],"summaryContent":null,"isExistSummaryContent":false,"warningFromEditor":null,"warningFromEditorTitle":null,"counters":{"comments":0,"favorites":0,"reposts":0,"views":509,"hits":1019,"reads":null,"online":0},"dateFavorite":0,"hitsCount":1019,"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":null,"url":"https://vc.ru/dev/1784409-dinamicheskoe-upravlenie-dostupom-roli-polzovatelei-na-letu-s-spring-boot-i-keycloak","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":1764922259,"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":1},{"id":2,"count":1}],"reactionId":0},"isNews":false,"source":null,"clusters":[],"donations":{"amount":0,"isDonated":false},"commentsSeenCount":null}}],"cursor":"PuR2GsZKFTvhhGtWDYrm6M/bBa8hRyGI6YitsrXO+VvHeLfWP7KpxEVZ52pHlyZg","isAnonymized":true}};