Files
LMS/docs/01-architecture.md

7.3 KiB
Raw Permalink Blame History

Архитектура системы

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: токен живёт в 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. Компоненты по сервисам

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, 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)

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) во все шаблоны.
  • Скрытые материалы и полное дерево материалов доступны только администратору.