/
Al
/
TESamples
Обзор
Документация
Войти
/
Al
/
TESamples
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
master
apps/spring/bff-agent/src/main/java/com/tokenexchange/bff/KeycloakClient.java
117 строк
5 KB
Al
Initial Commit
11 май 2026, 17:40
11 май 2026, 17:40
0a04577
Код
Авторство
О чём код?
package com.tokenexchange.bff; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; import com.tokenexchange.lib.ActorTokenCache; import org.springframework.beans.factory.annotation.Value; import org.springframework.http.MediaType; import org.springframework.stereotype.Component; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; import org.springframework.web.client.RestClient; /** * Тонкая обёртка над Keycloak token endpoint c Phase 5 failover-маршрутизацией. * * <p>ROPC и client_credentials проходят через {@link IdpRouter}: primary first, * secondary as fallback. Метрика {@code idp_failover_events_total{from,to}} * учитывает переключение. */ @Component public class KeycloakClient { private final RestClient http; private final IdpRouter router; private final String bffClientId; private final String agentClientId; private final ActorTokenCache actorCache; public KeycloakClient( RestClient http, IdpRouter router, ActorTokenCache actorCache, @Value("${tokenexchange.bff.client-id}") String bffClientId, @Value("${tokenexchange.agent.client-id}") String agentClientId ) { this.http = http; this.router = router; this.actorCache = actorCache; this.bffClientId = bffClientId; this.agentClientId = agentClientId; } public record TokenWithEndpoint(TokenResponse response, IdpRouter.IdpEndpoint endpoint) {} public TokenWithEndpoint passwordGrant(String username, String password, String scope) { var hold = new Object[1]; TokenResponse resp = router.execute(ep -> { hold[0] = ep; MultiValueMap<String, String> form = new LinkedMultiValueMap<>(); form.add("grant_type", "password"); form.add("client_id", bffClientId); form.add("client_secret", ep.bffClientSecret()); form.add("username", username); form.add("password", password); form.add("scope", scope); return post(ep.tokenUrl(), form); }); return new TokenWithEndpoint(resp, (IdpRouter.IdpEndpoint) hold[0]); } public TokenResponse clientCredentials(IdpRouter.IdpEndpoint ep) { // Phase 13: actor_token идёт через L1-cache + singleflight (FR-03.1, FR-04.1). // Cache key включает endpoint name, чтобы primary/secondary не коллизировали. String cacheKey = agentClientId + "@" + ep.name(); String cached = actorCache.getOrFetch(cacheKey, () -> { MultiValueMap<String, String> form = new LinkedMultiValueMap<>(); form.add("grant_type", "client_credentials"); form.add("client_id", agentClientId); form.add("client_secret", ep.agentClientSecret()); TokenResponse r = post(ep.tokenUrl(), form); long expiresIn = r.expiresIn() != null ? r.expiresIn() : 60L; return new ActorTokenCache.TokenFetchResult(r.accessToken(), expiresIn); }); return new TokenResponse(cached, null, null, "Bearer", null); } /** * Phase 8 / FR-15.1, FR-15.2: refresh user_at через refresh_token. * Возвращает null если refresh_token истёк / отозван (FR-15.2 — нужен re-auth). */ public TokenResponse refresh(IdpRouter.IdpEndpoint ep, String refreshToken) { return refresh(ep, refreshToken, bffClientId, ep.bffClientSecret()); } public TokenResponse refresh(IdpRouter.IdpEndpoint ep, String refreshToken, String clientId, String clientSecret) { MultiValueMap<String, String> form = new LinkedMultiValueMap<>(); form.add("grant_type", "refresh_token"); form.add("client_id", clientId); form.add("client_secret", clientSecret); form.add("refresh_token", refreshToken); try { return post(ep.tokenUrl(), form); } catch (org.springframework.web.client.HttpClientErrorException e) { // 400 invalid_grant — refresh_token истёк / отозван. return null; } } private TokenResponse post(String url, MultiValueMap<String, String> form) { return http.post() .uri(url) .contentType(MediaType.APPLICATION_FORM_URLENCODED) .body(form) .retrieve() .body(TokenResponse.class); } @JsonIgnoreProperties(ignoreUnknown = true) public record TokenResponse( @JsonProperty("access_token") String accessToken, @JsonProperty("refresh_token") String refreshToken, @JsonProperty("expires_in") Long expiresIn, @JsonProperty("token_type") String tokenType, @JsonProperty("scope") String scope ) {} }