Архитектура бэкенда с Keycloak¶
Рекомендация¶
Используем Java 21 + Spring Boot 3 + Keycloak + PostgreSQL как модульный монолит с подходом «сначала контракт». Текущий переходный стенд уже поднят как real Spring Boot backend с embedded H2, публичным Swagger UI и CRUD доходов/расходов/целей по адресу http://66.42.121.18/finguide-api/swagger-ui.html; legacy mock Swagger остаётся только для сравнения во время миграции.
Микросервисы на первом этапе не нужны. Для цели в 100k зарегистрированных пользователей обычно проще и надёжнее горизонтально масштабировать API без состояния, PostgreSQL, Redis, асинхронные обработчики и модели чтения. Модули проектируем как ограниченные контексты, чтобы позже можно было вынести аналитику, импорт/экспорт или уведомления без разрыва ядра финансового плана.
Общий поток¶
Фронтенд
-> Keycloak OIDC Authorization Code + PKCE
-> получает access_token / refresh_token
-> API бэкенда с Authorization: Bearer <JWT>
-> Бэкенд валидирует JWT через Keycloak JWKS
-> Бэкенд проверяет локального пользователя и доступ к плану
-> PostgreSQL / Redis / object storage
Бэкенд не хранит пароли и не владеет формами входа. Keycloak отвечает за идентификацию, учётные данные, MFA и пользовательские сессии. В demo/H2 режиме /api/v1/** временно открыт без JWT, чтобы фронтенд мог перейти с mock-запросов на реальные сервисы до подключения Keycloak.
Docker Compose deployment Keycloak¶
Для первого production/demo deployment Keycloak разворачивается через docker compose из deploy/keycloak/:
- сервис
keycloak-postgres— PostgreSQL 16 только для Keycloak, persistent volumekeycloak-postgres-data; - сервис
keycloak— Keycloak 26 на127.0.0.1:3094с public path/auth; .env.exampleсодержит только placeholders; реальные admin/DB secrets не коммитим и не пишем в логи;- healthcheck и
restart: unless-stoppedвключены для обоих сервисов; - custom theme
finguideоформляет login/account/password reset screens в стиле FinGuide; - backup/restore PostgreSQL описан в
deploy/keycloak/README.md.
PostgreSQL в этом stack обязателен. Embedded/dev-file DB Keycloak запрещён. Миграция финансовых данных FinGuide с H2 на PostgreSQL остаётся отдельной задачей, если только deployment design не потребует иначе.
Настройка Keycloak¶
realm: finguide
clients:
finguide-web публичный client, PKCE
finguide-admin confidential client для админских задач, если понадобится
roles:
user
admin
scopes/claims:
email
profile
preferred_username
В JWT нам нужны:
sub— стабильный внешний идентификатор пользователя;email;nameили параgiven_name+family_nameдля реального ФИО из регистрации;preferred_usernameкак безопасный fallback, если ФИО не заполнено;realm_access.rolesили роли клиента;email_verified.
Модули бэкенда¶
les13.finguide.backend
auth/ проверка JWT, SecurityConfig, CurrentUserProvider
users/ локальный бизнес-профиль
plans/ агрегат финансового плана и доступ
incomes/ CRUD источников дохода
expenses/ CRUD расходов и бюджетная классификация
goals/ цели и waterfall-приоритет
contributions/ фактические взносы в цели
pension/ настройки пенсии, preserve-capital и spend-down проекции
budget/ 50/30/20 и конверты
analytics/ предположения из Excel-модели, баланс, денежный поток, сбережения, дашборд, оценка финансового здоровья
scenarios/ снимки, корректировки и сравнение сценариев
importexport/ задачи JSON/CSV/XLSX/PDF
notifications/ производные уведомления, milestone-события и подсказки
Авторизация запроса¶
- Проверить подпись JWT и издателя токена (
issuer) через Keycloak JWKS. - Получить
subиз токена. - Найти или создать локального
users.keycloak_subject. - На каждом запросе к плану проверить владение планом или явно выданный доступ.
Пример:
GET /plans/{planId}/dashboard
Authorization: Bearer <JWT>
JWT.sub -> users.keycloak_subject -> владелец плана / доступ к плану -> дашборд
Профиль пользователя¶
Keycloak — источник идентичности. Бэкенд хранит бизнес-профиль.
users
- id uuid pk
- keycloak_subject text unique not null
- email text not null
- name text
- phone text
- avatar_url text
- age int
- gender text
- initial_balance numeric
- created_at timestamptz
- updated_at timestamptz
Архитектура расчётов¶
Excel-файл Модель_P---56630d2a-6465-4036-bd42-9117c7dc9bd6.xlsx — эталонная финансовая модель. Бэкенд должен воспроизвести её как детерминированные расчётные сервисы:
PlanState + ModelAssumptions
-> нормализовать суммы, ставки и даты
-> построить годовую шкалу
-> применить флаги активности по start/end year
-> применить годовые графики роста
-> посчитать доходы, расходы и расходы на цели
-> посчитать годовые сбережения и накопленный капитал
-> посчитать пенсионные варианты preserve-capital и spend-down
-> закешировать snapshots для dashboard/scenarios
Важные соглашения:
- API принимает расходы и цели положительными суммами.
- Поля API с суффиксом
Pct— процентные пункты (6= 6%). Расчётный код переводит их в десятичные ставки. analytics/cashflow— каноническая производная проекция. Дашборд, оценка финансового здоровья, сценарии и уведомления должны строиться поверх него, а не дублировать формулы.- Храним исходные входные данные и опциональные снимки расчётов; каждую промежуточную формулу сохранять не нужно, если нет требования аудита/кеша.
Основная модель хранения¶
users
plans
income_sources
expense_items
goals
contributions
pension_settings
budget_settings
budget_envelopes
scenarios
monthly_tracker_entries
export_jobs
notification_events
model_assumptions
analytics_snapshots
pension_projection_snapshots
Общие поля:
id uuid pk
plan_id uuid fk
created_at timestamptz
updated_at timestamptz
deleted_at timestamptz null
version int
version нужен для оптимистической блокировки и ETag / If-Match.
Масштабирование до 100k пользователей¶
100k зарегистрированных пользователей не означает автоматическую необходимость в микросервисах. Начинаем с:
Балансировщик нагрузки
-> 2-4 stateless-инстанса Spring Boot API
-> PostgreSQL primary + read replica при необходимости
-> Redis cache
-> 2+ инстанса Keycloak на PostgreSQL
-> асинхронные workers для analytics/export/notifications
Добавляем:
- кеш Redis для снимков дашборда/проекции, производных от
analytics/cashflow; - модели чтения:
plan_dashboard_snapshot,plan_projection_snapshot,financial_health_snapshot; - паттерн исходящих событий (outbox) для бизнес-событий;
- асинхронные задачи для пересчётов, экспорта и уведомлений.
Что выносить в сервисы позже¶
analytics-worker
читает события PlanChanged
пересчитывает projections и scenarios
import-export-worker
генерирует XLSX/PDF/CSV
notification-worker
создаёт reminders, milestones и tips
Финансовое ядро (plans, incomes, expenses, goals, pension) сначала лучше держать вместе: много расчётов требует консистентного состояния одного плана.
Безопасность¶
- Время жизни access token: 5-15 минут.
- Ротация refresh token в Keycloak.
- Бэкенд проверяет аудиторию и издателя токена (
audience/issuer). - Ограничение частоты запросов для auth-like и import/export методов API.
- Журнал аудита для критичных изменений плана.
- Персональные данные хранить минимально; секреты — только в переменных окружения или secret manager.
Реализация в текущем backend¶
В рамках задачи #18 добавлен production auth boundary без удаления demo режима:
- Spring Security OAuth2 Resource Server валидирует issuer/JWKS из
KEYCLOAK_ISSUER_URI; KEYCLOAK_AUDIENCEпо умолчаниюfinguide-api, неподходящийaudдаёт403 FORBIDDEN;JWT.subмаппится вuser_profiles.keycloak_subject, профиль создаётся лениво при первом запросе;- имя/email локального профиля синхронизируются из текущего JWT, поэтому authenticated API не возвращает seeded placeholder
Александр Петроввместо ФИО зарегистрированного пользователя; GET /api/v1/meвозвращает текущий бизнес-профиль;- первый authenticated
GET /api/v1/plans/currentтранзакционно создаёт ровно один current plan дляuser_profiles.id, клонируя явный persisted seed plan22222222-2222-4222-8222-222222222222; повторные запросы возвращают тот же пользовательский план; - frontend при восстановлении OIDC-сессии не должен переиспользовать anonymous demo cache и не должен показывать seeded demo/default профиль до ответа authenticated
/plans/current; на время bootstrap показывается нейтральный loader; - доступ к
/api/v1/plans/{planId}/...проверяется по владельцуfinancial_plans.owner_user_id; рольadminможет читать план для диагностики; authenticated demo mode больше не даёт cross-plan bypass; - операции записи income/expense/goal проходят ту же проверку доступа и пишут audit log без секретов;
FINGUIDE_DEMO_MODE=trueсохраняет прежний no-auth demo/H2 режим для локальных тестов и текущего публичного стенда до запуска Keycloak.