CP4D 토큰 요청의 api_key 본문 방식 지원
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을 확인했습니다.
This commit is contained in:
2026-04-06 16:51:26 +09:00
parent 3961dc8ca2
commit fd3af15e54
3 changed files with 44 additions and 20 deletions

View File

@@ -1,4 +1,5 @@
using System.Net.Http;
using System.Net;
using System.Net.Http;
using System.Net.Http.Json;
using System.Text.Json;
@@ -35,38 +36,53 @@ internal sealed class Cp4dTokenService
var cacheKey = $"{cp4dUrl}|{username}";
// 캐시 확인 — 만료 1분 전까지 유효
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";
var body = new { username, password };
string? json = null;
HttpStatusCode? statusCode = null;
string? lastErrorBody = null;
using var req = new HttpRequestMessage(HttpMethod.Post, tokenUrl)
async Task<bool> TryAuthorizeAsync(object body, string bodyKind)
{
Content = JsonContent.Create(body)
};
using var req = new HttpRequestMessage(HttpMethod.Post, tokenUrl)
{
Content = JsonContent.Create(body)
};
// CP4D는 자체 서명 인증서를 사용할 수 있으므로 TLS 오류 무시 옵션 (사내 환경)
using var resp = await _http.SendAsync(req, ct);
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;
}
if (!resp.IsSuccessStatusCode)
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))
{
var errBody = await resp.Content.ReadAsStringAsync(ct);
LogService.Warn($"CP4D 토큰 발급 실패: {resp.StatusCode} - {errBody}");
LogService.Warn($"CP4D 토큰 발급 최종 실패: {statusCode} - {lastErrorBody}");
return null;
}
var json = await resp.Content.ReadAsStringAsync(ct);
using var doc = JsonDocument.Parse(json);
// CP4D 응답 형식: {"token": "...", "_messageCode_": "...", "message": "..."}
if (!doc.RootElement.TryGetProperty("token", out var tokenProp))
{
LogService.Warn("CP4D 응답에 token 필드가 없습니다.");
@@ -74,12 +90,10 @@ internal sealed class Cp4dTokenService
}
var token = tokenProp.GetString();
if (string.IsNullOrEmpty(token)) return null;
if (string.IsNullOrEmpty(token))
return null;
// 토큰 만료 시간 — CP4D 기본 12시간, 안전하게 11시간으로 설정
var expiry = DateTime.UtcNow.AddHours(11);
// _messageCode_에서 만료 정보가 있으면 파싱 시도
if (doc.RootElement.TryGetProperty("accessTokenExpiry", out var expiryProp) &&
expiryProp.TryGetInt64(out var expiryMs))
{
@@ -105,12 +119,18 @@ internal sealed class Cp4dTokenService
public static void InvalidateToken(string cp4dUrl, string username)
{
var cacheKey = $"{cp4dUrl}|{username}";
lock (_lock) { _cache.Remove(cacheKey); }
lock (_lock)
{
_cache.Remove(cacheKey);
}
}
/// <summary>모든 캐시된 토큰을 초기화합니다.</summary>
public static void ClearAllTokens()
{
lock (_lock) { _cache.Clear(); }
lock (_lock)
{
_cache.Clear();
}
}
}