6.4 KiB
6.4 KiB
🏗 Архитектура системы
Общая схема
graph TD
Client["🌐 Браузер"] -->|HTTPS| Caddy["Caddy Proxy"]
Caddy -->|:80| Frontend["Frontend<br/>(Apache httpd:alpine)"]
Caddy -->|/api/*| Backend["Backend<br/>(Spring Boot 3.2.5)"]
Backend --> TenantRouter{"TenantRoutingDataSource"}
TenantRouter -->|swsu.zuev.company| DB1["PostgreSQL<br/>swsu_db"]
TenantRouter -->|mgu.zuev.company| DB2["PostgreSQL<br/>mgu_db"]
TenantRouter -->|...| DBn["PostgreSQL<br/>tenant_n_db"]
Backend -->|Метрики, Логи, Трейсы| OTel["OpenTelemetry Collector"]
OTel --> SigNoz["SigNoz"]
Компоненты
Frontend (Apache httpd:alpine)
- Тип: Статические файлы (HTML/CSS/JS)
- Контейнер:
httpd:alpine— лёгкий Apache HTTP Server - Порт: 80
- Содержание: Три изолированных интерфейса —
admin/,teacher/,student/ - JS-модули: Vanilla JavaScript с ES6 Modules (
import/export)
Backend (Spring Boot 3.2.5)
- Тип: REST API сервер
- Язык: Java 17
- Порт: 8080 (внутренний)
- ORM: Hibernate (JPA),
ddl-auto=none - Миграции: Flyway (программный запуск при подключении тенанта)
- Аутентификация: bcrypt (через
BCryptPasswordEncoder), UUID-токены
PostgreSQL
- Версия:
postgres:alpine3.23 - Локально: Одна БД
app_db(тенантdefault) - Продакшн: Множество БД, по одной на каждый университет (тенант)
Caddy (реверс-прокси)
- Расположение:
../caddy-proxy/ - Назначение: TLS-терминация, маршрутизация запросов к backend/frontend
- Домен:
*.zuev.company
Мультитенантная архитектура
Ключевая особенность системы — изоляция данных каждого университета в отдельной БД PostgreSQL.
Принцип работы
sequenceDiagram
participant Browser as Браузер
participant Interceptor as TenantInterceptor
participant Context as TenantContext
participant Router as TenantRoutingDataSource
participant DB as PostgreSQL
Browser->>Interceptor: GET /api/users<br/>Host: swsu.zuev.company
Interceptor->>Interceptor: resolveTenant("swsu.zuev.company") → "swsu"
Interceptor->>Context: setCurrentTenant("swsu")
Note over Interceptor: Проверка: hasTenant("swsu")?
Interceptor-->>Browser: 404 если тенант не найден
Note over Context,Router: Обработка запроса контроллером
Router->>Router: determineCurrentLookupKey() → "swsu"
Router->>DB: SQL запрос к swsu_db
DB-->>Browser: Ответ с данными
Ключевые классы
| Класс | Назначение |
|---|---|
TenantInterceptor |
Извлекает поддомен из заголовка Host и определяет тенант |
TenantContext |
ThreadLocal-хранилище имени текущего тенанта |
TenantRoutingDataSource |
Наследует AbstractRoutingDataSource, маршрутизирует запросы к нужной БД |
TenantDataSourceConfig |
Загружает конфигурацию тенантов из JSON-файла, создаёт HikariCP пулы |
TenantConfigWatcher |
Периодически (каждые 30 сек) перечитывает tenants.json, синхронизирует тенантов |
ConfigMapUpdater |
Обновляет Kubernetes ConfigMap при добавлении/удалении тенанта через API |
TenantConfig |
POJO с параметрами тенанта: name, domain, url, username, password |
Определение тенанта
Логика определения тенанта по заголовку Host:
| Host | Результат |
|---|---|
swsu.zuev.company |
swsu |
mgu.zuev.company |
mgu |
localhost |
default |
localhost:8080 |
default |
192.168.1.1 |
default |
Конфигурация тенантов
Список тенантов хранится в JSON-файле:
- Локально:
backend/tenants.json - Продакшн: Kubernetes ConfigMap
tenants-config, монтируется в/config/tenants.json
Формат:
[
{
"name": "ЮЗГУ",
"domain": "swsu",
"url": "jdbc:postgresql://db-host:5432/swsu_db",
"username": "dbuser",
"password": "dbpass"
}
]
Жизненный цикл тенанта
- Добавление через API:
POST /api/database/tenants→ создаёт HikariCP пул → запускает Flyway миграции → обновляет ConfigMap - Синхронизация подов:
TenantConfigWatcherкаждые 30 сек проверяетtenants.json→ добавляет новые / удаляет отсутствующие тенанты - Удаление:
DELETE /api/database/tenants/{domain}→ закрывает пул → обновляет ConfigMap
Fallback при отсутствии тенантов
Если при запуске нет ни одного настроенного тенанта:
- Проверяется наличие
spring.datasource.url→ создаётся тенантdefault - Если datasource тоже нет → создаётся H2 in-memory заглушка для инициализации Spring JPA
Аутентификация
Система использует простую модель аутентификации без JWT или Spring Security фильтров:
- Клиент отправляет
POST /api/auth/loginсusernameиpassword - Backend проверяет пароль через
BCryptPasswordEncoder - При успехе возвращается:
- UUID-токен (для заголовка
Authorization: Bearer) - Роль пользователя (
ADMIN,TEACHER,STUDENT) - Redirect URL (
/admin/,/teacher/,/student/)
- UUID-токен (для заголовка
- Токен хранится в
localStorageна клиенте