/
Al
/
TESamples
Обзор
Документация
Войти
/
Al
/
TESamples
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
master
apps/spring/chain-service/src/main/java/com/tokenexchange/service/EchoController.java
153 строки
7 KB
Al
Initial Commit
11 май 2026, 17:40
11 май 2026, 17:40
0a04577
Код
Авторство
О чём код?
package com.tokenexchange.service; import com.tokenexchange.lib.RevocationCache; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Value; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.security.oauth2.jwt.Jwt; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.client.RestClient; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; import org.springframework.web.server.ResponseStatusException; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.Map; /** * Звено цепочки backend-сервисов. Валидирует JWT через Spring Security OAuth2 * Resource Server, читает claims, при наличии next-hop URL — пробрасывает * <b>тот же</b> токен дальше без повторного обмена. Реализует §1.1 Рекомендаций * для клиента: «Обмен токеном ровно один раз — на входе в цепочку» * (docs/client-recommendations.md#11). * * <p>Эндпоинты: * GET /api/echo — валидация JWT + проксирование того же Bearer в NEXT_URL. * GET /api/critical — операция, разрешённая только агентам с act.agent_type ∈ * {tool, workflow} (§4 Рекомендаций, * docs/client-recommendations.md#4). * * <p>Конфигурация — через application.yml: * service.name = a|b|c * service.next-url = http://service-X:8080/api/echo (опц.; пусто — терминал) */ @RestController @RequestMapping("/api") public class EchoController { private static final Logger log = LoggerFactory.getLogger(EchoController.class); private final RestClient http; private final String serviceName; private final String nextUrl; private final RevocationCache revocationCache; private final ClockSkewObserver clockSkew; public EchoController( RestClient http, RevocationCache revocationCache, ClockSkewObserver clockSkew, @Value("${service.name}") String serviceName, @Value("${service.next-url:}") String nextUrl ) { this.http = http; this.revocationCache = revocationCache; this.clockSkew = clockSkew; this.serviceName = serviceName; this.nextUrl = nextUrl == null ? "" : nextUrl.trim(); } /** * Phase 12 / FR-01a.6: act-based authorization policy demo. * * Критичная операция — разрешена только агентам типов {tool, workflow}. * llm-agent отвергается. policy опирается на claim {@code act.agent_type}, * который заполняется Phase-11 SPI (act-mapper). */ @GetMapping("/critical") public Map<String, Object> critical(@AuthenticationPrincipal Jwt jwt) { if (revocationCache.isRevoked(jwt.getSubject())) { throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "session_revoked"); } @SuppressWarnings("unchecked") Map<String, Object> act = jwt.getClaim("act"); if (act == null) { log.info("[{}] /api/critical denied: act claim missing", serviceName); throw new ResponseStatusException(HttpStatus.FORBIDDEN, "act_claim_required"); } Object agentType = act.get("agent_type"); if (!"tool".equals(agentType) && !"workflow".equals(agentType)) { log.info("[{}] /api/critical denied: agent_type='{}' not in allowlist (FR-01a.6)", serviceName, agentType); throw new ResponseStatusException(HttpStatus.FORBIDDEN, "agent_type_not_allowed:" + agentType); } Map<String, Object> result = new LinkedHashMap<>(); result.put("service", serviceName); result.put("operation", "critical"); result.put("performed_by", Map.of( "sub", jwt.getSubject(), "user", jwt.getClaimAsString("preferred_username"), "act", act )); result.put("status", "ok"); return result; } @GetMapping("/echo") public Map<String, Object> echo(@AuthenticationPrincipal Jwt jwt) { // Phase 6 / FR-05.4: deny-list check после успешной валидации JWT. if (revocationCache.isRevoked(jwt.getSubject())) { log.info("[{}] denying revoked sub={}", serviceName, jwt.getSubject()); throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "session_revoked"); } // Phase 8 / FR-15.9: наблюдение clock skew (только метрика). long skew = clockSkew.observe(jwt); Map<String, Object> result = new LinkedHashMap<>(); result.put("clock_skew_seconds", skew); result.put("service", serviceName); result.put("sub", jwt.getSubject()); result.put("preferred_username", jwt.getClaimAsString("preferred_username")); result.put("scope", jwt.getClaimAsString("scope")); result.put("aud", jwt.getAudience()); result.put("azp", jwt.getClaimAsString("azp")); result.put("jti", jwt.getId()); result.put("iss", jwt.getIssuer().toString()); result.put("exp", jwt.getExpiresAt() != null ? jwt.getExpiresAt().getEpochSecond() : null); // Если есть next-hop — пробрасываем ТОТ ЖЕ Authorization-заголовок. if (!nextUrl.isEmpty()) { String authHeader = currentAuthHeader(); log.info("[{}] forwarding the SAME bearer to {}", serviceName, nextUrl); try { @SuppressWarnings("unchecked") Map<String, Object> downstream = http.get() .uri(nextUrl) .header(HttpHeaders.AUTHORIZATION, authHeader) .retrieve() .body(Map.class); result.put("downstream", downstream); } catch (Exception e) { Map<String, Object> err = new HashMap<>(); err.put("error", e.getClass().getSimpleName()); err.put("message", e.getMessage()); result.put("downstream", err); } } else { result.put("downstream", null); } return result; } private String currentAuthHeader() { var attrs = (ServletRequestAttributes) RequestContextHolder.currentRequestAttributes(); return attrs.getRequest().getHeader(HttpHeaders.AUTHORIZATION); } }