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 ITokenStore _tokenStore; private readonly ILogger _logger; public TokenRefreshHandler(ITokenStore tokenStore, ILogger logger) { _tokenStore = tokenStore; _logger = logger; } protected override async Task SendAsync( HttpRequestMessage request, CancellationToken cancellationToken) { // 요청에 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 AuthTokenPair(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; } }