Files
LMS/docs/01-architecture.md

168 lines
8.0 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Архитектура системы
## 1. Контекст (какие внешние участники и системы)
```mermaid
flowchart LR
user["Преподаватель / студент<br/>(браузер)"]
nginx["nginx<br/>lms.our-place.ru<br/>TLS, reverse proxy"]
web["web BFF<br/>Thymeleaf + Bootstrap<br/>:8090"]
gw["gateway<br/>Spring Cloud Gateway<br/>:8080"]
id["identity-service<br/>:8081"]
ac["academic-service<br/>:8082"]
pr["progress-service<br/>:8083"]
db[("PostgreSQL 18<br/>схемы identity / academic / progress")]
fs[("Файловое хранилище<br/>content-assets, submissions")]
user -->|HTTPS| nginx
nginx -->|HTTP 127.0.0.1:8090| web
web -->|Bearer JWT| gw
gw --> id
gw --> ac
gw --> pr
id --> db
ac --> db
pr --> db
ac --> fs
pr --> fs
pr -->|REST, внутренняя сеть| ac
pr -->|REST, внутренняя сеть| id
id -.->|JWKS: /api/v1/auth/jwks.json| gw
id -.->|JWKS| ac
id -.->|JWKS| pr
```
- Браузер никогда не получает JWT: access-токен живёт в HTTP-сессии web-сервиса
(`lms.token`), а в браузер уходит только cookie сессии `LMS_SESSION`
(`HttpOnly`, `SameSite=Lax`, 7 дней sliding). Access-токен — 15 минут,
обновляется автоматически за 2 минуты до истечения по refresh-токену;
cookie `Max-Age` перевыпускается на каждом ответе.
- Все REST-запросы web проксирует на gateway, добавляя `Authorization: Bearer`.
- Gateway проверяет подпись токена по JWKS identity и передаёт вниз заголовки
`X-Account-Id`, `X-Account-Login`, `X-Account-Role`.
- Каждый сервис дополнительно валидирует JWT самостоятельно (resource server).
## 2. Компоненты по сервисам
```mermaid
flowchart TB
subgraph web["web (BFF, :8090)"]
wc["@Controller: AuthPage, Learn, Student,<br/>AdminGroup/Lab/Student/Distribute/Defense/Users, ContentAdmin<br/>@RestController: ContentAssetProxy"]
wi["Interceptor'ы:<br/>Csrf, NoCachePage, MustChangePassword, Auth, Admin"]
wapi["ApiClient (RestClient)<br/>+ DTO"]
wmd["MarkdownRenderer<br/>(libs:markdown)"]
wc --> wapi
wc --> wmd
wi --> wc
end
subgraph gw["gateway (:8080)"]
routes["Маршруты<br/>identity / academic / progress"]
filter["JwtAuthenticationFilter<br/>order -100"]
filter --> routes
end
subgraph id["identity-service (:8081)"]
idc["AuthController, GroupController, StudentController"]
ids["AuthService, GroupService, Passwords,<br/>StudentImportParser, CredentialsExporter"]
idr["AccountRepository, GroupRepository,<br/>EnrollmentRepository, StudentRepository, SettingRepository"]
idc --> ids --> idr
end
subgraph ac["academic-service (:8082)"]
acc["LabController, ContentPublicController,<br/>ContentAdminController, ContentOrderController"]
acs["LabService, ContentService,<br/>LabImportParser, AssetStorage"]
acr["LabRepository, LabLevelRepository, VariantRepository,<br/>SemesterRepository, Content*Repository"]
acc --> acs --> acr
end
subgraph pr["progress-service (:8083)"]
prc["ProgressController, SubmissionController, DefenseAdminController,<br/>DefenseMyController, StudentPurgeController, GroupLabDataController"]
prs["ProgressService, SubmissionService, DefenseService,<br/>GradePublicationService, StudentPurgeService"]
prr["VariantAssignmentRepository, StudentLabLevelRepository,<br/>LabGradeRepository, GradeCommentRepository, CommentTemplateRepository,<br/>SubmissionRepository, DefenseSessionRepository,<br/>GradePublicationRepository, GradePublicationStudentRepository"]
prcl["AcademicClient, IdentityClient"]
prc --> prs
prs --> prr
prs --> prcl
end
wapi -->|HTTP| routes
routes --> idc
routes --> acc
routes --> prc
prcl -->|GET /api/v1/labs/{id}| acc
prcl -->|GET /api/v1/groups и /{id}| idc
```
## 3. Модули Gradle
```mermaid
flowchart LR
web[":web"] --> common[":libs:common-http"]
web --> md[":libs:markdown"]
gw[":services:gateway"] --> sec[":libs:security-jwt"]
gw --> common
id[":services:identity-service"] --> common
id --> sec
ac[":services:academic-service"] --> common
ac --> sec
ac --> md
pr[":services:progress-service"] --> common
pr --> sec
tests[":libs:test-support"] -.-> id
tests -.-> ac
tests -.-> pr
```
| Модуль | Назначение |
|---|---|
| `:libs:common-http` | единый формат ошибок (`ApiError`), иерархия `LmsException`, `LmsExceptionHandler`, генератор UUIDv7 (`Ids`) |
| `:libs:security-jwt` | свойства JWT, парсинг RSA-ключей, автоконфигурация `JwtEncoder`/`JwtDecoder`/`PasswordEncoder`, enum `LmsRole` |
| `:libs:markdown` | markdown → безопасный HTML (OWASP-санитайзер), таблицы, спойлеры `:::spoiler` |
| `:libs:test-support` | константы для интеграционных тестов с PostgreSQL |
## 4. Аутентификация и авторизация
```mermaid
flowchart TD
a["Клиент: POST /api/v1/auth/login"] --> b["gateway: путь публичный<br/>(login/setup/health/jwks)"]
b --> c["identity: AuthService.login"]
c --> d{"BCrypt совпал?"}
d -->|нет| e["401 + ApiError"]
d -->|да| f["JwtEncoder (RSA-2048, kid=lms)<br/>claims: sub, login, role, exp"]
f --> g["AuthResponse{token,...}"]
h["Клиент: запрос к защищённому API<br/>Authorization: Bearer ..."] --> i["gateway JwtAuthenticationFilter"]
i --> j{"Токен валиден по JWKS?"}
j -->|нет| k["401 Unauthorized"]
j -->|да| l["Добавляет X-Account-Id/Login/Role"]
l --> m["Сервис: SecurityConfig<br/>oauth2ResourceServer(jwt)"]
m --> n{"@PreAuthorize<br/>ROLE_ADMIN / isAuthenticated"}
n -->|нет| o["403 Forbidden"]
n -->|да| p["Контроллер → сервис → БД"]
```
- Роли: `ADMIN`, `TEACHER`, `STUDENT`; конвертер ролей читает claim `role` как `ROLE_<role>`.
- Публичные пути: `/actuator/**`, `/api/v1/auth/{health,setup,login,refresh,jwks.json}`,
`/api/v1/content/tree`, `/api/v1/content/{chapter}/{section}/{article}`.
`/api/v1/content/assets/**` gateway пропускает без JWT (публичные
`ALL`-активы), но academic проверяет видимость статьи и для курсовых или
ролевых активов требует токен. Файл без привязки к статье — `404` всем,
кроме админа (secure-by-default).
- RSA-ключи identity хранятся в таблице `settings` и публикуются в JWKS.
## 5. Рендеринг страниц и защита форм (web)
```mermaid
flowchart LR
req["HTTP-запрос"] --> csrf["CsrfInterceptor /**<br/>GET: выдаёт token в сессию<br/>POST: сверяет _csrf"]
csrf --> nocahe["NoCachePageInterceptor /**<br/>(страницы без кэша; /css, /js исключены)"]
nocahe --> must["MustChangePasswordInterceptor /**<br/>(кроме /change-password, /logout, /login, статики)"]
must --> auth["AuthInterceptor<br/>/admin/**, /student/**, /change-password"]
auth --> adm["AdminInterceptor /admin/**"]
adm --> ctrl["Контроллер → ApiClient → gateway"]
```
- `GlobalModelAdvice` кладёт `currentUser` (SessionUser) во все шаблоны.
- Скрытые материалы и полное дерево материалов доступны только администратору.