8.0 KiB
8.0 KiB
Архитектура системы
1. Контекст (какие внешние участники и системы)
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-токену; cookieMax-Ageперевыпускается на каждом ответе. - Все REST-запросы web проксирует на gateway, добавляя
Authorization: Bearer. - Gateway проверяет подпись токена по JWKS identity и передаёт вниз заголовки
X-Account-Id,X-Account-Login,X-Account-Role. - Каждый сервис дополнительно валидирует JWT самостоятельно (resource server).
2. Компоненты по сервисам
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
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. Аутентификация и авторизация
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; конвертер ролей читает claimroleкак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)
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) во все шаблоны.- Скрытые материалы и полное дерево материалов доступны только администратору.