162 lines
7.3 KiB
Markdown
162 lines
7.3 KiB
Markdown
# Архитектура системы
|
||
|
||
## 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: токен живёт в HTTP-сессии web-сервиса
|
||
(`lms.token`), а в браузер уходит только cookie сессии (`HttpOnly`, 30 дней).
|
||
- Все 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`, `STUDENT`; конвертер ролей читает claim `role` как `ROLE_<role>`.
|
||
- Публичные пути: `/actuator/**`, `/api/v1/auth/{health,setup,login,jwks.json}`,
|
||
`/api/v1/content/tree`, `/api/v1/content/assets/**`,
|
||
`/api/v1/content/{chapter}/{section}/{article}`.
|
||
- 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) во все шаблоны.
|
||
- Скрытые материалы и полное дерево материалов доступны только администратору.
|