Some checks failed
Release Gate / gate (push) Has been cancelled
IBM 계열 vLLM 연결 점검 결과 일부 CP4D 인증 엔드포인트가 username+password 대신 username+api_key JSON 본문을 요구하는 것을 확인했습니다. Cp4dTokenService가 먼저 username/password를 시도하고 실패 시 username/api_key로 자동 재시도하도록 보강했으며 README와 DEVELOPMENT 문서를 2026-04-06 16:55 (KST) 기준으로 갱신했습니다. Release 빌드 경고 0 오류 0을 확인했습니다.
137 lines
4.8 KiB
C#
137 lines
4.8 KiB
C#
using System.Net;
|
|
using System.Net.Http;
|
|
using System.Net.Http.Json;
|
|
using System.Text.Json;
|
|
|
|
namespace AxCopilot.Services;
|
|
|
|
/// <summary>
|
|
/// IBM Cloud Pak for Data (CP4D) 토큰 발급 및 캐싱 서비스.
|
|
/// CP4D의 /icp4d-api/v1/authorize 엔드포인트에서 Bearer 토큰을 발급받고,
|
|
/// 만료 전까지 캐싱하여 재사용합니다.
|
|
/// </summary>
|
|
internal sealed class Cp4dTokenService
|
|
{
|
|
private static readonly HttpClient _http = new()
|
|
{
|
|
Timeout = TimeSpan.FromSeconds(15)
|
|
};
|
|
|
|
// 캐시: (cp4dUrl + username) → (token, expiry)
|
|
private static readonly Dictionary<string, (string Token, DateTime Expiry)> _cache = new();
|
|
private static readonly object _lock = new();
|
|
|
|
/// <summary>
|
|
/// CP4D 토큰을 발급받거나 캐시에서 반환합니다.
|
|
/// </summary>
|
|
/// <param name="cp4dUrl">CP4D 서버 URL (예: https://cpd-host.example.com)</param>
|
|
/// <param name="username">CP4D 사용자 이름</param>
|
|
/// <param name="password">CP4D 비밀번호 또는 API 키</param>
|
|
/// <param name="ct">취소 토큰</param>
|
|
/// <returns>Bearer 토큰 문자열. 실패 시 null.</returns>
|
|
public static async Task<string?> GetTokenAsync(string cp4dUrl, string username, string password, CancellationToken ct = default)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(cp4dUrl) || string.IsNullOrWhiteSpace(username))
|
|
return null;
|
|
|
|
var cacheKey = $"{cp4dUrl}|{username}";
|
|
|
|
lock (_lock)
|
|
{
|
|
if (_cache.TryGetValue(cacheKey, out var cached) && cached.Expiry > DateTime.UtcNow.AddMinutes(1))
|
|
return cached.Token;
|
|
}
|
|
|
|
try
|
|
{
|
|
var tokenUrl = cp4dUrl.TrimEnd('/') + "/icp4d-api/v1/authorize";
|
|
string? json = null;
|
|
HttpStatusCode? statusCode = null;
|
|
string? lastErrorBody = null;
|
|
|
|
async Task<bool> TryAuthorizeAsync(object body, string bodyKind)
|
|
{
|
|
using var req = new HttpRequestMessage(HttpMethod.Post, tokenUrl)
|
|
{
|
|
Content = JsonContent.Create(body)
|
|
};
|
|
|
|
using var resp = await _http.SendAsync(req, ct);
|
|
statusCode = resp.StatusCode;
|
|
var rawBody = await resp.Content.ReadAsStringAsync(ct);
|
|
if (!resp.IsSuccessStatusCode)
|
|
{
|
|
lastErrorBody = rawBody;
|
|
LogService.Warn($"CP4D 토큰 발급 실패({bodyKind}): {resp.StatusCode} - {rawBody}");
|
|
return false;
|
|
}
|
|
|
|
json = rawBody;
|
|
LogService.Info($"CP4D 토큰 발급 성공({bodyKind})");
|
|
return true;
|
|
}
|
|
|
|
var authorized = await TryAuthorizeAsync(new { username, password }, "username+password");
|
|
if (!authorized && !string.IsNullOrWhiteSpace(password))
|
|
authorized = await TryAuthorizeAsync(new { username, api_key = password }, "username+api_key");
|
|
|
|
if (!authorized || string.IsNullOrWhiteSpace(json))
|
|
{
|
|
LogService.Warn($"CP4D 토큰 발급 최종 실패: {statusCode} - {lastErrorBody}");
|
|
return null;
|
|
}
|
|
|
|
using var doc = JsonDocument.Parse(json);
|
|
|
|
if (!doc.RootElement.TryGetProperty("token", out var tokenProp))
|
|
{
|
|
LogService.Warn("CP4D 응답에 token 필드가 없습니다.");
|
|
return null;
|
|
}
|
|
|
|
var token = tokenProp.GetString();
|
|
if (string.IsNullOrEmpty(token))
|
|
return null;
|
|
|
|
var expiry = DateTime.UtcNow.AddHours(11);
|
|
if (doc.RootElement.TryGetProperty("accessTokenExpiry", out var expiryProp) &&
|
|
expiryProp.TryGetInt64(out var expiryMs))
|
|
{
|
|
expiry = DateTimeOffset.FromUnixTimeMilliseconds(expiryMs).UtcDateTime;
|
|
}
|
|
|
|
lock (_lock)
|
|
{
|
|
_cache[cacheKey] = (token, expiry);
|
|
}
|
|
|
|
LogService.Info($"CP4D 토큰 발급 완료: {cp4dUrl} (만료: {expiry:yyyy-MM-dd HH:mm} UTC)");
|
|
return token;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
LogService.Error($"CP4D 토큰 발급 오류: {ex.Message}");
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/// <summary>특정 CP4D 서버의 캐시된 토큰을 무효화합니다.</summary>
|
|
public static void InvalidateToken(string cp4dUrl, string username)
|
|
{
|
|
var cacheKey = $"{cp4dUrl}|{username}";
|
|
lock (_lock)
|
|
{
|
|
_cache.Remove(cacheKey);
|
|
}
|
|
}
|
|
|
|
/// <summary>모든 캐시된 토큰을 초기화합니다.</summary>
|
|
public static void ClearAllTokens()
|
|
{
|
|
lock (_lock)
|
|
{
|
|
_cache.Clear();
|
|
}
|
|
}
|
|
}
|