Программирование

Provisioning пользователей в Keycloak и DDD микросервисах Spring Boot

Как организовать provisioning пользователей между PostgreSQL микросервиса и Keycloak в DDD + Hexagonal архитектуре на Java Spring Boot. Transactional Outbox паттерн, event-driven подход, data consistency при сбоях. Лучшие практики от экспертов.

3 ответа 1 просмотр

Как правильно организовать provisioning пользователей между базой данных микросервиса и Keycloak в архитектуре DDD + Hexagonal Microservices на Java Spring Boot?

Контекст:

  • BFF-сервис оркестрирует запросы фронтенда (Confidential Client, хранит токен).
  • Customer Service управляет доменными данными в PostgreSQL.
  • Keycloak отвечает за аутентификацию и идентификацию.

Проблема: При регистрации нового клиента нужно создать запись в PostgreSQL customer-service и provision пользователя в Keycloak для входа в систему.

Вопросы:

  1. Хорошая ли практика обрабатывать provisioning Keycloak внутри Infrastructure Adapter в customer-service?
  2. Как обеспечить согласованность данных (data consistency), если Keycloak недоступен во время регистрации? Стоит ли использовать Transactional Outbox Pattern?

Рассматривается event-driven подход: публикация события CustomerCreated из customer-service, но неясно, кто должен потреблять его для вызова Admin API Keycloak.

Provisioning пользователей между базой данных микросервиса Customer Service в PostgreSQL и Keycloak в архитектуре DDD + Hexagonal Microservices на Java Spring Boot лучше организовать через event-driven подход с Transactional Outbox Pattern. Это позволяет сохранить запись клиента в PostgreSQL даже при недоступности Keycloak, а provisioning выполнить асинхронно через событие CustomerCreated и отдельный idempotent consumer, вызывающий Admin API Keycloak. Такой метод обеспечивает data consistency без нарушения bounded context и избегает прямой зависимости от внешнего сервиса в Infrastructure Adapter.


Содержание


Provisioning пользователей в Keycloak: проблемы и лучшие практики

Представьте: клиент регистрируется через фронтенд, BFF оркестрирует запрос, Customer Service создает агрегат Customer в PostgreSQL. Но без пользователя в Keycloak вход в систему невозможен. Прямая синхронная интеграция с Keycloak Admin API нарушает принципы микросервисов — coupling растет, транзакции выходят за bounded context, а при сбое Keycloak вся регистрация рушится.

Почему это проблема в DDD? Customer — доменная сущность с lifecycle’ом в своем контексте. Provisioning в Keycloak — это identity provisioning, ближе к Identity Provider. Лучшие практики, описанные Chris Richardson на Microservices.io, рекомендуют eventual consistency через события. А Gary Archer на Stack Overflow подчеркивает: храните в микросервисе только доменные атрибуты (имя, email), а полную идентификацию — в Keycloak для GDPR и простоты.

Коротко: используйте upsert в Keycloak (создать или обновить по ID), idempotent операции и retry. Но не синхронно!


Интеграция Keycloak Spring Boot в DDD микросервисы

Keycloak Spring Boot — отличный выбор для аутентификации в микросервисах. Настройте spring-boot-starter-oauth2-resource-server и keycloak-spring-boot-starter в BFF и сервисах. В application.yml:

yaml
spring:
 security:
 oauth2:
 resourceserver:
 jwt:
 issuer-uri: http://keycloak:8080/realms/myrealm
keycloak:
 realm: myrealm
 auth-server-url: http://keycloak:8080

В DDD + Hexagonal это значит: аутентификация через port (Security Port), а provisioning — не в домене. BFF как confidential client хранит токен, делегирует регистрацию в Customer Service. Keycloak users API вызывается только асинхронно.

Проблема с пользователями: Keycloak хранит credentials, роли, но не бизнес-данные. Синхронизация через Keycloak Admin Client — норма, но требует осторожности с consistency.

А что если Keycloak недоступен? Здесь вступает магия событий.


Обработка регистрации клиента: BFF, Customer Service и Keycloak

BFF принимает POST /register с данными (email, password). Валидирует, генерирует ID. Затем вызывает Customer Service API: POST /customers.

В Customer Service (Hexagonal):

  1. Application Service создает Customer Aggregate.
  2. Repository сохраняет в PostgreSQL.
  3. Вместо прямого вызова Keycloak — публикует событие.

BFF не ждет provisioning — возвращает 202 Accepted сразу. Клиент проверяет статус по /customers/{id}/status позже.

Почему не в BFF provisioning? BFF — оркестратор, не владелец домена. Customer Service знает о lifecycle клиента.

Диаграмма потока регистрации с BFF, Customer Service и Keycloak через события

Эта схема из Microservices.io показывает, как избежать blocking операций.


Transactional Outbox паттерн для data consistency

Да, Transactional Outbox — золотой стандарт для вашего случая. При создании Customer:

java
@Transactional
public Customer createCustomer(CreateCustomerCommand cmd) {
 Customer customer = Customer.create(cmd.getId(), cmd.getEmail(), cmd.getName());
 customerRepository.save(customer);
 
 // Outbox в той же транзакции
 outboxRepository.save(OutgoingEvent.builder()
 .aggregateType("Customer")
 .aggregateId(customer.getId())
 .eventType("CustomerCreated")
 .payload(toJson(customer))
 .build());
 
 return customer;
}

Outbox — таблица outbox_events в PostgreSQL с колонками id, aggregate_id, type, payload, processed=false.

Relay (отдельный scheduler, @Scheduled) поллит unprocessed события, публикует в Kafka/RabbitMQ, маркирует processed.

Преимущества: ACID в БД, no distributed transactions. Даже если Kafka down — событие ждет.

Chris Richardson детально разбирает это для микросервисов.


Event-driven подход: событие CustomerCreated и потребители

Кто потребляет CustomerCreated? Не Customer Service! Создайте отдельный сервис: Keycloak Provisioning Service (или модуль).

Consumer (Kafka @KafkaListener):

java
@KafkaListener(topics = "customer-events")
public void handleCustomerCreated(String payload) {
 CustomerCreatedEvent event = fromJson(payload, CustomerCreatedEvent.class);
 keycloakAdminClient.users().create(realm, toKeycloakUser(event));
}

Idempotency: проверяйте exists по userId в Keycloak перед create. Используйте if-match ETag в API.

Топики: customer-events.v1. Схемы в Avro для evolution.

Альтернатива: CDC (Debezium на PostgreSQL) — но Outbox проще для DDD событий.

Вопрос: а если consumer упадет? Retry с dead-letter queue.


Infrastructure Adapter в Hexagonal Architecture

Нет, provisioning Keycloak не в Infrastructure Adapter Customer Service. Почему?

  • Нарушает SRP: сервис фокусируется на домене.
  • Coupling к внешнему API — тестить сложно.
  • Gary Archer предупреждает: это дает лишние права, дублирует логику.

Вместо: Port для Domain Events. Adapter публикует в broker. Provisioning — в другом bounded context (Identity Service).

Hexagonal чист: Domain → Application → Ports → Adapters (JPA, Kafka Outbox, REST/Keycloak в consumer).

Просто. Масштабируемо.


Обеспечение согласованности при недоступности Keycloak

Keycloak down? Customer сохраняется в PostgreSQL — регистрация успешна. Consumer retry’ит событие (Kafka retries, Spring Retry).

Стратегия:

  • Retry policy: exponential backoff, max 5 attempts.
  • Dead letter: ручная обработка.
  • Compensating: если нужно, событие CustomerVerificationRequired.
  • Monitoring: Prometheus + Grafana на outbox lag.

Eventual consistency — норма в микросервисах. Клиент увидит “pending” статус, пока не provisioned.

Без Outbox пришлось бы rollback всей транзакции — плохо для UX.

PROSELYTE показывает реализацию с Kafka.


Примеры реализации на Java Spring Boot

Зависимости:

xml
<dependency>
 <groupId>org.springframework.boot</groupId>
 <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
 <groupId>org.springframework.kafka</groupId>
 <artifactId>spring-kafka</artifactId>
</dependency>
<dependency>
 <groupId>org.keycloak</groupId>
 <artifactId>keycloak-admin-client</artifactId>
 <version>25.0.0</version>
</dependency>

Outbox Entity:

java
@Entity
@Table(name = "outbox_events")
public class OutboxEvent {
 @Id @GeneratedValue private UUID id;
 private String aggregateType;
 private String aggregateId;
 private String eventType;
 private String payload;
 private boolean processed;
 // getters/setters
}

Relay:

java
@Component
public class OutboxRelay {
 @Scheduled(fixedDelay = 5000)
 public void publishPendingEvents() {
 List<OutboxEvent> pending = outboxRepo.findByProcessedFalse();
 for (OutboxEvent event : pending) {
 kafkaTemplate.send("customer-events", event.getPayload());
 event.setProcessed(true);
 outboxRepo.save(event);
 }
 }
}

Consumer в Provisioning Service — аналогично. Тестируйте с Testcontainers (Keycloak + Kafka + Postgres).

Для полной картины смотрите Piotr’s TechBlog.


Источники

  1. Stack Overflow — Обсуждение provisioning пользователей между микросервисом и Keycloak от Gary Archer: https://stackoverflow.com/questions/79880139/how-should-user-provisioning-be-handled-between-the-microservice-database-and-ke
  2. Microservices.io — Transactional Outbox Pattern для reliable messaging в микросервисах от Chris Richardson: https://microservices.io/patterns/data/transactional-outbox.html
  3. PROSELYTE — Реализация Transactional Outbox с Spring Boot и Kafka: https://proselyte.net/tutorials/spring-boot-kafka-transactional-outbox-pattern/
  4. Piotr’s TechBlog — Примеры Transactional Outbox в Spring Boot микросервисах: https://piotrminkowski.com/2023/07/12/transactional-outbox-with-spring-boot-and-kafka/

Заключение

Provisioning в Keycloak через Transactional Outbox и event-driven с отдельным consumer — оптимальный путь для DDD + Hexagonal на Spring Boot. Избегайте Infrastructure Adapter в Customer Service, фокусируйтесь на eventual consistency с retry и idempotency. Это масштабируемо, надежно и соответствует best practices от экспертов вроде Richardson и Archer. Начните с PoC — увидите, как упростится жизнь при outage’ах.

G

В архитектуре с Keycloak рекомендуется централизовать пользовательские данные в Keycloak для упрощения аутентификации Keycloak и соблюдения GDPR, храня в customer-service только доменные атрибуты.

Provisioning Keycloak внутри Infrastructure Adapter customer-service — плохая практика, так как дает сервису лишние привилегии и дублирует логику Keycloak.

Лучше реализовать регистрацию через OAuth flow в Keycloak с верификацией (email), а затем синхронизировать с customer-service upsert-ами.

Для data consistency используйте retry-логику, а не transactional outbox, чтобы избежать усложнения; при недоступности Keycloak клиент повторяет запрос.

Это обеспечивает простоту в spring boot микросервисах без event-driven overhead.

Chris Richardson / Архитектор программного обеспечения

Transactional outbox паттерн идеален для provisioning пользователей в DDD микросервисах: при создании Customer в customer-service сохраняйте бизнес-данные и событие CustomerCreated в outbox-таблице в одной транзакции PostgreSQL.

Relay-процесс публикует событие в Kafka, а consumer вызывает Keycloak Admin API для создания пользователя.

Это гарантирует consistency даже при недоступности Keycloak — событие retry’ится асинхронно.

Не размещайте provisioning в Infrastructure Adapter customer-service; выносите в отдельный сервис.

В Keycloak Spring Boot реализуйте idempotent consumer по aggregate ID для at-least-once семантики.

Диаграмма паттерна Transactional Outbox: запись в БД (бизнес-данные + outbox), relay → message broker → consumer
Авторы
G
Старший разработчик программного обеспечения
Chris Richardson / Архитектор программного обеспечения
Архитектор программного обеспечения
Источники
Stack Overflow / Платформа вопросов и ответов
Платформа вопросов и ответов
Портал по паттернам микросервисов
Проверено модерацией
НейроОтветы
Модерация