143 lines
6.4 KiB
Markdown
143 lines
6.4 KiB
Markdown
# 🏗 Архитектура системы
|
||
|
||
## Общая схема
|
||
|
||
```mermaid
|
||
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.
|
||
|
||
### Принцип работы
|
||
|
||
```mermaid
|
||
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`
|
||
|
||
Формат:
|
||
```json
|
||
[
|
||
{
|
||
"name": "ЮЗГУ",
|
||
"domain": "swsu",
|
||
"url": "jdbc:postgresql://db-host:5432/swsu_db",
|
||
"username": "dbuser",
|
||
"password": "dbpass"
|
||
}
|
||
]
|
||
```
|
||
|
||
### Жизненный цикл тенанта
|
||
|
||
1. **Добавление через API:** `POST /api/database/tenants` → создаёт HikariCP пул → запускает Flyway миграции → обновляет ConfigMap
|
||
2. **Синхронизация подов:** `TenantConfigWatcher` каждые 30 сек проверяет `tenants.json` → добавляет новые / удаляет отсутствующие тенанты
|
||
3. **Удаление:** `DELETE /api/database/tenants/{domain}` → закрывает пул → обновляет ConfigMap
|
||
|
||
### Fallback при отсутствии тенантов
|
||
|
||
Если при запуске нет ни одного настроенного тенанта:
|
||
1. Проверяется наличие `spring.datasource.url` → создаётся тенант `default`
|
||
2. Если datasource тоже нет → создаётся H2 in-memory заглушка для инициализации Spring JPA
|
||
|
||
---
|
||
|
||
## Аутентификация
|
||
|
||
Система использует **простую модель аутентификации** без JWT или Spring Security фильтров:
|
||
|
||
1. Клиент отправляет `POST /api/auth/login` с `username` и `password`
|
||
2. Backend проверяет пароль через `BCryptPasswordEncoder`
|
||
3. При успехе возвращается:
|
||
- UUID-токен (для заголовка `Authorization: Bearer`)
|
||
- Роль пользователя (`ADMIN`, `TEACHER`, `STUDENT`)
|
||
- Redirect URL (`/admin/`, `/teacher/`, `/student/`)
|
||
4. Токен хранится в `localStorage` на клиенте
|