namespace TaxBaik.Web.Services; using System.Net; using System.Text.Json; /// /// HTTP 요청 시 자동으로 access token을 추가하고, /// 401 응답을 받으면 refresh token으로 새 토큰을 획득한 후 재시도합니다. /// SOLID: Single Responsibility - 토큰 갱신 로직만 담당 /// public class TokenRefreshHandler : DelegatingHandler { private readonly IServiceProvider _serviceProvider; private readonly ILogger _logger; public TokenRefreshHandler(IServiceProvider serviceProvider, ILogger logger) { _serviceProvider = serviceProvider; _logger = logger; } protected override async Task SendAsync( HttpRequestMessage request, CancellationToken cancellationToken) { // 최신 Scoped ITokenStore 실시간 해석 (Scope Capture 차단 및 기존 Blazor 회로 수명 공유) var tokenStore = Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService(_serviceProvider); // 요청에 access token 추가 if (!string.IsNullOrEmpty(tokenStore.AccessToken)) { request.Headers.Authorization = new("Bearer", tokenStore.AccessToken); } var response = await base.SendAsync(request, cancellationToken); // 401 응답이면 토큰 갱신 시도 if (response.StatusCode == HttpStatusCode.Unauthorized) { if (!string.IsNullOrEmpty(tokenStore.RefreshToken)) { var newTokenPair = await RefreshTokenAsync(tokenStore.RefreshToken, request, cancellationToken); if (newTokenPair != null) { // TokenStore에 토큰 저장 tokenStore.AccessToken = newTokenPair.AccessToken; tokenStore.RefreshToken = newTokenPair.RefreshToken; tokenStore.TokenExpiryTicks = DateTime.UtcNow.AddSeconds(newTokenPair.ExpiresIn).Ticks; // 새 토큰으로 재요청 request.Headers.Authorization = new("Bearer", newTokenPair.AccessToken); response = await base.SendAsync(request, cancellationToken); } else { _logger.LogWarning("토큰 갱신 실패 - 로그아웃"); tokenStore.Clear(); } } } return response; } private async Task RefreshTokenAsync(string refreshToken, HttpRequestMessage originalRequest, CancellationToken ct) { try { // 원래 요청의 호스트 정보 추출 var authority = originalRequest.RequestUri?.Authority ?? "localhost:5001"; var scheme = originalRequest.RequestUri?.Scheme ?? "http"; using var httpClient = new HttpClient(); var refreshUri = new Uri($"{scheme}://{authority}/taxbaik/api/auth/refresh"); var json = JsonSerializer.Serialize(new { refreshToken }); var content = new StringContent(json, System.Text.Encoding.UTF8, "application/json"); var response = await httpClient.PostAsync(refreshUri, content, ct); if (!response.IsSuccessStatusCode) { _logger.LogWarning($"Token refresh failed with status {response.StatusCode}"); return null; } var responseContent = await response.Content.ReadAsStringAsync(ct); var result = JsonSerializer.Deserialize(responseContent, new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); return result != null ? new WasmAuthTokenPair(result.AccessToken, result.RefreshToken, result.ExpiresIn) : null; } catch (Exception ex) { _logger.LogError(ex, "Exception during token refresh"); return null; } } } internal class AuthTokenResponse { public string AccessToken { get; set; } = string.Empty; public string RefreshToken { get; set; } = string.Empty; public int ExpiresIn { get; set; } }