/
Al
/
TESamples
Обзор
Документация
Войти
/
Al
/
TESamples
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
master
apps/spring/token-exchange-lib/src/main/java/com/tokenexchange/lib/TokenExchangeClient.java
131 строка
6 KB
Al
Initial Commit
11 май 2026, 17:40
11 май 2026, 17:40
0a04577
Код
Авторство
О чём код?
package com.tokenexchange.lib; import io.micrometer.core.instrument.Counter; import io.micrometer.core.instrument.MeterRegistry; import io.micrometer.core.instrument.Timer; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.http.MediaType; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; import org.springframework.web.client.RestClient; import java.util.concurrent.TimeUnit; /** * Клиент эндпоинта {@code POST /realms/{realm}/protocol/openid-connect/token} * с {@code grant_type=urn:ietf:params:oauth:grant-type:token-exchange} (RFC 8693). * * <p>Реализует §1.1 Рекомендаций для клиента * (docs/client-recommendations.md#11): вызывается <b>ровно один раз</b> * на входе в backend-цепочку. Downstream-сервисы повторно не обмениваются — * проксируют тот же токен. См. также §5 «Сужение прав агента» * (docs/client-recommendations.md#5) — здесь же задаются итоговые * {@code scope} и {@code audience}. */ public class TokenExchangeClient { public static final String GRANT_TOKEN_EXCHANGE = "urn:ietf:params:oauth:grant-type:token-exchange"; private static final Logger log = LoggerFactory.getLogger(TokenExchangeClient.class); private final RestClient http; private final String tokenUrl; private final String clientId; private final String clientSecret; private final AuditLogger audit; private final MeterRegistry meterRegistry; // optional, may be null public TokenExchangeClient(RestClient http, String tokenUrl, String clientId, String clientSecret, AuditLogger audit) { this(http, tokenUrl, clientId, clientSecret, audit, null); } public TokenExchangeClient(RestClient http, String tokenUrl, String clientId, String clientSecret, AuditLogger audit, MeterRegistry meterRegistry) { this.http = http; this.tokenUrl = tokenUrl; this.clientId = clientId; this.clientSecret = clientSecret; this.audit = audit; this.meterRegistry = meterRegistry; } /** Performs the token exchange against the constructor-configured endpoint. */ public TokenExchangeResponse exchange(TokenExchangeRequest req) { return exchange(req, tokenUrl, clientId, clientSecret); } /** * Performs the token exchange against the supplied endpoint/credentials — * used by Phase 5 failover (BFF picks endpoint at request-time via IdpRouter). */ public TokenExchangeResponse exchange(TokenExchangeRequest req, String tokenUrlOverride, String clientIdOverride, String clientSecretOverride) { long start = System.nanoTime(); MultiValueMap<String, String> form = new LinkedMultiValueMap<>(); form.add("grant_type", GRANT_TOKEN_EXCHANGE); form.add("client_id", clientIdOverride); form.add("client_secret", clientSecretOverride); form.add("subject_token", req.subjectToken()); form.add("subject_token_type", req.subjectTokenType()); if (req.actorToken() != null) { form.add("actor_token", req.actorToken()); form.add("actor_token_type", req.actorTokenType()); } form.add("requested_token_type", req.requestedTokenType()); if (req.audience() != null) form.add("audience", req.audience()); if (req.scope() != null) form.add("scope", req.scope()); try { TokenExchangeResponse resp = http.post() .uri(tokenUrlOverride) .contentType(MediaType.APPLICATION_FORM_URLENCODED) .body(form) .retrieve() .body(TokenExchangeResponse.class); long tookNanos = System.nanoTime() - start; long tookMs = tookNanos / 1_000_000; audit.tokenExchange(clientIdOverride, req.audience(), req.scope(), "ok", tookMs, null); recordMetric("ok", req.audience(), tookNanos, clientIdOverride); log.debug("TE ok aud={} scope={} took={}ms", req.audience(), req.scope(), tookMs); return resp; } catch (Exception e) { long tookNanos = System.nanoTime() - start; long tookMs = tookNanos / 1_000_000; audit.tokenExchange(clientIdOverride, req.audience(), req.scope(), "error", tookMs, e.getMessage()); recordMetric("error", req.audience(), tookNanos, clientIdOverride); throw new TokenExchangeException("Token exchange failed: " + e.getMessage(), e); } } private void recordMetric(String result, String audience, long tookNanos, String clientIdLabel) { if (meterRegistry == null) return; String aud = audience == null ? "" : audience; Timer.builder("te.request.duration") .description("Token Exchange request duration") .tag("result", result) .tag("audience", aud) .tag("client_id", clientIdLabel) .publishPercentileHistogram() .register(meterRegistry) .record(tookNanos, TimeUnit.NANOSECONDS); Counter.builder("te.requests") .description("Token Exchange total requests") .tag("result", result) .tag("audience", aud) .tag("client_id", clientIdLabel) .register(meterRegistry) .increment(); } }