/
Al
/
TESamples
Обзор
Документация
Войти
/
Al
/
TESamples
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
master
docker-compose.yml
407 строк
19 KB
Александр Седых
added guide-not-framework.md, fixed problem
13 май 2026, 14:04
13 май 2026, 14:04
fdd9cf0
Код
Авторство
О чём код?
# ============================================================= # TokenExchange — корневой compose # # Фаза 2: Postgres + Keycloak (FR-12) + smoke-init (FR-12.7) + # bff-agent + service-a/b/c (single-TE паттерн §1.1.1). # # bff-agent выполняет ровно один TE и проксирует один и тот же # delegated_at через цепочку service-a -> service-b -> service-c. # # Запуск: # cp .env.example .env # docker compose up -d --build # ============================================================= name: tokenexchange services: postgres: # Хранилище Keycloak. Закреплённая версия (NFR-06: никаких `latest`). # На уничтожении volume теряются все realms — учитывается `make down-v`. image: postgres:16.6-alpine restart: unless-stopped environment: POSTGRES_USER: ${POSTGRES_USER:-keycloak} POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-keycloak-dev} POSTGRES_DB: ${POSTGRES_DB:-keycloak} volumes: - postgres_data:/var/lib/postgresql/data healthcheck: # Keycloak ждёт `service_healthy`, иначе валится на старте. test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-keycloak} -d ${POSTGRES_DB:-keycloak}"] interval: 5s timeout: 5s retries: 10 # Redis: pub/sub-шина для событий инвалидации (event-listener-redis SPI → chain-services) # и L2-кеш делегированных токенов. redis: image: redis:7.4-alpine restart: unless-stopped # Demo-режим: без RDB-снапшотов и AOF (`--save ""`, `--appendonly no`). # Pub/sub-события эфемерны и в persist'е не нуждаются. В production — # настраивается persistence + repl + auth (см. README клиента). command: ["redis-server", "--save", "", "--appendonly", "no"] ports: - "${REDIS_PORT:-6379}:6379" healthcheck: test: ["CMD", "redis-cli", "ping"] interval: 5s timeout: 3s retries: 10 keycloak: build: context: ./keycloak dockerfile: Dockerfile args: KC_VERSION: 26.3.5 image: tokenexchange/keycloak:26.3.5 restart: unless-stopped # `start-dev` — dev-режим (HTTP, прогретые JIT-параметры выключены). # В production используется `start` + production-flags (TLS, hostname-strict, # production cache). `--import-realm` загружает все JSON из /opt/keycloak/data/import. command: ["start-dev", "--import-realm"] environment: KC_DB: postgres KC_DB_URL: jdbc:postgresql://postgres:5432/${POSTGRES_DB:-keycloak} KC_DB_USERNAME: ${POSTGRES_USER:-keycloak} KC_DB_PASSWORD: ${POSTGRES_PASSWORD:-keycloak-dev} # Bootstrap-учётка для admin-консоли. Dev-defaults, в production — # из секретохранилища с обязательной ротацией после первого логина. KC_BOOTSTRAP_ADMIN_USERNAME: ${KC_BOOTSTRAP_ADMIN_USERNAME:-admin} KC_BOOTSTRAP_ADMIN_PASSWORD: ${KC_BOOTSTRAP_ADMIN_PASSWORD:-admin-dev} # Включает /health, /metrics, /ready на management-порту (отдельно от 8080, # чтобы не зависеть от глобальной auth). Используется Prometheus-scrape # и compose-healthcheck. KC_HEALTH_ENABLED: "true" KC_HTTP_MANAGEMENT_PORT: "9000" # Включаем ТОЛЬКО RFC 8693 (`token-exchange-standard`). Legacy `token-exchange` # — анти-паттерн и НЕ включается (см. рекомендации клиенту, §1.4). # admin-fine-grained-authz нужен для policy-based access в admin-console. KC_FEATURES: "token-exchange-standard,admin-fine-grained-authz" # DEBUG только на token-эндпоинте и grants — видны запросы TE с параметрами. # Не INFO на root, чтобы не утопить полезные сообщения от SPI. KC_LOG_LEVEL: "INFO,org.keycloak.protocol.oidc.endpoints.TokenEndpoint:DEBUG,org.keycloak.protocol.oidc.grants:DEBUG" # Phase 7 (FR-13): стабильный iss = host.docker.internal:8080. # - С хоста (browser) этот alias резолвится Docker Desktop'ом в localhost. # - Изнутри контейнеров — резолвится в host-gateway (см. extra_hosts ниже). # Это решает iss-mismatch между browser-flow и back-channel TE. KC_HOSTNAME: "http://host.docker.internal:${KEYCLOAK_PORT:-8080}" # В dev — false, чтобы Keycloak принимал любой Host-header (в т. ч. # `keycloak:8080` от контейнеров). В production — `true` + единый # публичный hostname через TLS. KC_HOSTNAME_STRICT: "false" ports: - "${KEYCLOAK_PORT:-8080}:8080" - "${KEYCLOAK_MANAGEMENT_PORT:-9000}:9000" volumes: # Realm-export'ы (FR-12.1): декларативно описывают клиентов, scope, mappers, # TE-policies. Меняются в git, не через admin-UI. - ./keycloak/realms:/opt/keycloak/data/import:ro depends_on: postgres: condition: service_healthy redis: condition: service_healthy keycloak-init: # FR-12.7: smoke-init выполняет полный TE-flow (ROPC → client_credentials → TE) # и проверяет claims в delegated_at. Если конфиг сломан — выходит с !=0, # все BFF/chain-сервисы НЕ стартуют (depends_on: service_completed_successfully). image: alpine:3.20 restart: "no" depends_on: postgres: condition: service_healthy keycloak: condition: service_started volumes: - ./keycloak/init:/init:ro environment: # Внутренний URL Keycloak (back-channel) — отдельно от публичного `KC_HOSTNAME`. KC_BASE: "http://keycloak:8080" KC_MGMT: "http://keycloak:9000" REALM: "${REALM_PRIMARY:-realm-primary}" BFF_CLIENT_ID: "bff-agent" BFF_SECRET: "bff-agent-dev-secret" AGENT_CLIENT_ID: "agent-runtime" AGENT_SECRET: "agent-runtime-dev-secret" TEST_USER: "alice" TEST_USER_PASSWORD: "alice-dev" # Smoke сверяет, что выпущенный delegated_at имеет: # aud ⊇ EXPECTED_AUDIENCE (FR-01b.2) # scope = EXPECTED_SCOPE (FR-01b.1) EXPECTED_AUDIENCE: "internal-services" EXPECTED_SCOPE: "orders:read" entrypoint: ["sh", "-c"] command: - | set -e apk add --no-cache --quiet curl jq bash >/dev/null exec bash /init/smoke.sh # ---------- Phase 2: Java сервисы ---------- bff-agent: build: context: ./apps/spring dockerfile: Dockerfile args: MODULE: bff-agent image: tokenexchange/bff-agent:0.1.0 restart: unless-stopped # host.docker.internal в alias-картe контейнера резолвится в host-gateway. # Нужно, чтобы JWT, выпущенный с iss=http://host.docker.internal:... мог # быть верифицирован: BFF дёргает discovery + JWKS по тому же hostname. extra_hosts: - "host.docker.internal:host-gateway" environment: BFF_PORT: "8080" # Back-channel URL (внутри docker network) — отдельно от публичного. KC_BASE_URL: "http://keycloak:8080" REALM: "${REALM_PRIMARY:-realm-primary}" BFF_CLIENT_ID: "bff-agent" BFF_CLIENT_SECRET: "bff-agent-dev-secret" AGENT_CLIENT_ID: "agent-runtime" AGENT_CLIENT_SECRET: "agent-runtime-dev-secret" # Верхняя граница прав, которые пользователь даёт BFF при логине (ROPC/PKCE). # Из этого набора TE может down-scope'нуть подмножество (FR-01b.1). BFF_LOGIN_SCOPE: "openid orders:read orders:write payments:write" # ОБЩИЙ audience для всей цепочки A→B→C (FR-01b.2). Меняется через .env, # чтобы тестировать invalid_target и расхождение aud по цепочке. TE_AUDIENCE: "internal-services" # Down-scoped scope в выпускаемом delegated_at. Меньше login-scope. # Поменять на `admin` для отрицательного up-scoping-теста. TE_SCOPE: "orders:read" CHAIN_ENTRY_URL: "http://service-a:8080/api/echo" # Phase 7 / FR-13: SPA-flow redirects (nginx-proxy на порту 8090). # Должны быть зарегистрированы в Keycloak-клиенте `spa-public`. SPA_REDIRECT_URI: "${SPA_REDIRECT_URI:-http://localhost:8090/auth/callback}" SPA_POST_LOGIN_REDIRECT: "${SPA_POST_LOGIN_REDIRECT:-http://localhost:8090/}" # Публичный URL Keycloak (тот, что видит браузер). ДОЛЖЕН совпадать # с `KC_HOSTNAME` Keycloak — иначе iss-mismatch при валидации. KC_PUBLIC_BASE_URL: "${KC_PUBLIC_BASE_URL:-http://host.docker.internal:8080}" KC_ADMIN_USERNAME: "${KC_BOOTSTRAP_ADMIN_USERNAME:-admin}" KC_ADMIN_PASSWORD: "${KC_BOOTSTRAP_ADMIN_PASSWORD:-admin-dev}" # MaxRAMPercentage=75: JVM подбирает heap по реальному cgroup-лимиту контейнера. # UseContainerSupport включён по умолчанию в JDK 11+, оставлен явно ради читаемости. JAVA_OPTS: "-XX:+UseContainerSupport -XX:MaxRAMPercentage=75" ports: - "${BFF_PORT:-8081}:8080" depends_on: # Ждём успешного smoke (FR-12.7). Если init упал — BFF не стартует. keycloak-init: condition: service_completed_successfully # service-a / -b / -c — три инстанса одного chain-service-образа с разными # SERVICE_NAME и NEXT_URL. Цепочка нужна, чтобы продемонстрировать инвариант # single-TE-at-edge: ОДИН и тот же delegated_at валидируется КАЖДЫМ хопом # и форвардится дальше БЕЗ повторного TE (один jti на всю цепочку). service-a: build: context: ./apps/spring dockerfile: Dockerfile args: MODULE: chain-service image: tokenexchange/chain-service:0.1.0 restart: unless-stopped extra_hosts: - "host.docker.internal:host-gateway" environment: SERVICE_PORT: "8080" SERVICE_NAME: "service-a" # issuer-uri для Spring Security OAuth2 Resource Server (autodiscovery JWKS). KC_ISSUER_URI: "http://keycloak:8080/realms/${REALM_PRIMARY:-realm-primary}" # Multi-issuer trust (FR-06.7): дополнительно доверяем токенам с public iss # (host.docker.internal), потому что user_at, выпущенный через browser-flow, # будет иметь именно его. Без этого 401 invalid_token. KC_TRUSTED_ISSUERS: "http://host.docker.internal:${KEYCLOAK_PORT:-8080}/realms/${REALM_PRIMARY:-realm-primary}" # Куда форвардить тот же Bearer. У service-c пусто → терминал цепочки. NEXT_URL: "http://service-b:8080/api/echo" JAVA_OPTS: "-XX:+UseContainerSupport -XX:MaxRAMPercentage=75" ports: - "8082:8080" depends_on: keycloak-init: condition: service_completed_successfully service-b: # Тот же образ chain-service:0.1.0. Build не нужен — image берётся из локального # реестра, собранного у service-a. image: tokenexchange/chain-service:0.1.0 restart: unless-stopped extra_hosts: - "host.docker.internal:host-gateway" environment: SERVICE_PORT: "8080" SERVICE_NAME: "service-b" KC_ISSUER_URI: "http://keycloak:8080/realms/${REALM_PRIMARY:-realm-primary}" KC_TRUSTED_ISSUERS: "http://host.docker.internal:${KEYCLOAK_PORT:-8080}/realms/${REALM_PRIMARY:-realm-primary}" NEXT_URL: "http://service-c:8080/api/echo" JAVA_OPTS: "-XX:+UseContainerSupport -XX:MaxRAMPercentage=75" ports: - "8083:8080" depends_on: service-a: condition: service_started service-c: image: tokenexchange/chain-service:0.1.0 restart: unless-stopped extra_hosts: - "host.docker.internal:host-gateway" environment: SERVICE_PORT: "8080" SERVICE_NAME: "service-c" KC_ISSUER_URI: "http://keycloak:8080/realms/${REALM_PRIMARY:-realm-primary}" KC_TRUSTED_ISSUERS: "http://host.docker.internal:${KEYCLOAK_PORT:-8080}/realms/${REALM_PRIMARY:-realm-primary}" # Терминальный хоп: пустой NEXT_URL → не форвардим, возвращаем результат вверх. NEXT_URL: "" JAVA_OPTS: "-XX:+UseContainerSupport -XX:MaxRAMPercentage=75" ports: - "8084:8080" depends_on: service-a: condition: service_started # ---------- Phase 3: Python зеркало (тот же контракт single-TE) ---------- # Python-стек реализует тот же контракт, что Java. Полезен для: # 1) Доказать, что delegated_at не зависит от языка валидации (PyJWT и # Spring Security приходят к одинаковому решению). # 2) Сшивать цепочки кросс-стек: например, service-a (Java) → service-b-py. bff-agent-py: build: context: ./apps/python dockerfile: Dockerfile args: MODULE: bff_agent image: tokenexchange/bff-agent-py:0.1.0 restart: unless-stopped environment: KC_BASE_URL: "http://keycloak:8080" REALM: "${REALM_PRIMARY:-realm-primary}" BFF_CLIENT_ID: "bff-agent" BFF_CLIENT_SECRET: "bff-agent-dev-secret" AGENT_CLIENT_ID: "agent-runtime" AGENT_CLIENT_SECRET: "agent-runtime-dev-secret" BFF_LOGIN_SCOPE: "openid orders:read orders:write payments:write" TE_AUDIENCE: "internal-services" TE_SCOPE: "orders:read" CHAIN_ENTRY_URL: "http://service-a-py:8080/api/echo" SERVICE_NAME: "bff-agent-py" ports: - "${BFF_PORT_PY:-18091}:8080" depends_on: keycloak-init: condition: service_completed_successfully service-a-py: build: context: ./apps/python dockerfile: Dockerfile args: MODULE: chain_service image: tokenexchange/chain-service-py:0.1.0 restart: unless-stopped extra_hosts: - "host.docker.internal:host-gateway" environment: SERVICE_NAME: "service-a-py" KC_ISSUER_URI: "http://keycloak:8080/realms/${REALM_PRIMARY:-realm-primary}" # Phase 7: trust также public iss (когда user_at выпущен через browser-flow). KC_TRUSTED_ISSUERS: "http://host.docker.internal:${KEYCLOAK_PORT:-8080}/realms/${REALM_PRIMARY:-realm-primary}" NEXT_URL: "http://service-b-py:8080/api/echo" ports: - "8092:8080" depends_on: keycloak-init: condition: service_completed_successfully service-b-py: image: tokenexchange/chain-service-py:0.1.0 restart: unless-stopped extra_hosts: - "host.docker.internal:host-gateway" environment: SERVICE_NAME: "service-b-py" KC_ISSUER_URI: "http://keycloak:8080/realms/${REALM_PRIMARY:-realm-primary}" # Phase 7: trust также public iss (когда user_at выпущен через browser-flow). KC_TRUSTED_ISSUERS: "http://host.docker.internal:${KEYCLOAK_PORT:-8080}/realms/${REALM_PRIMARY:-realm-primary}" NEXT_URL: "http://service-c-py:8080/api/echo" ports: - "8093:8080" depends_on: service-a-py: condition: service_started service-c-py: image: tokenexchange/chain-service-py:0.1.0 restart: unless-stopped extra_hosts: - "host.docker.internal:host-gateway" environment: SERVICE_NAME: "service-c-py" KC_ISSUER_URI: "http://keycloak:8080/realms/${REALM_PRIMARY:-realm-primary}" # Phase 7: trust также public iss (когда user_at выпущен через browser-flow). KC_TRUSTED_ISSUERS: "http://host.docker.internal:${KEYCLOAK_PORT:-8080}/realms/${REALM_PRIMARY:-realm-primary}" NEXT_URL: "" ports: - "8094:8080" depends_on: service-a-py: condition: service_started # ---------- Phase 7: SPA + BFF consent UI (FR-13) ---------- # vanilla JS + nginx. FR-13.9: build выполняется в multi-stage Dockerfile, # локальный Node.js на хосте не нужен. nginx-proxy маршрутизирует: # /auth/*, /api/* → bff-agent # / → static SPA files spa: build: context: ./apps/spa dockerfile: Dockerfile image: tokenexchange/spa:0.1.0 restart: unless-stopped environment: # passed-through для документации; nginx сам не использует. BFF_INTERNAL_URL: "http://bff-agent:8080" ports: - "${SPA_PORT:-8090}:8080" depends_on: bff-agent: condition: service_started # ---------- Vanilla demo (фреймворк-независимый TE-client) ---------- # CLI-приложение на чистом Java + Apache HTTP Client 5. # Демонстрирует полный flow: ROPC → client_credentials → TE → chain call. # # Запуск (one-shot, не сервер): # docker compose run --rm vanilla-demo # docker compose run --rm vanilla-demo --chain-url http://service-a:8080/api/echo vanilla-demo: build: context: ./apps/vanilla dockerfile: Dockerfile image: tokenexchange/vanilla-demo:0.1.0 restart: "no" environment: KC_BASE_URL: "http://keycloak:8080" KC_REALM: "${REALM_PRIMARY:-realm-primary}" BFF_CLIENT_ID: "bff-agent" BFF_CLIENT_SECRET: "bff-agent-dev-secret" AGENT_CLIENT_ID: "agent-runtime" AGENT_CLIENT_SECRET: "agent-runtime-dev-secret" BFF_LOGIN_SCOPE: "openid orders:read orders:write" TE_AUDIENCE: "internal-services" TE_SCOPE: "orders:read" CHAIN_ENTRY_URL: "http://service-a:8080/api/echo" DEMO_USERNAME: "alice" DEMO_PASSWORD: "alice-dev" depends_on: keycloak-init: condition: service_completed_successfully volumes: postgres_data: