using System.IdentityModel.Tokens.Jwt; using System.Security.Claims; using Microsoft.AspNetCore.Components.Authorization; namespace TaxBaik.Web.Services; public class CustomAuthenticationStateProvider : AuthenticationStateProvider { private readonly ILocalStorageService _localStorage; private readonly ITokenStore _tokenStore; private readonly AuthService _authService; private readonly ILogger _logger; public CustomAuthenticationStateProvider( ILocalStorageService localStorage, ITokenStore tokenStore, AuthService authService, ILogger logger) { _localStorage = localStorage; _tokenStore = tokenStore; _authService = authService; _logger = logger; } public override async Task GetAuthenticationStateAsync() { try { var accessToken = _tokenStore.AccessToken; // TokenStore가 비어있으면 localStorage에서 복원 (페이지 리로드 후) if (string.IsNullOrEmpty(accessToken)) { var storedToken = await _localStorage.GetItemAsStringAsync("accessToken"); if (!string.IsNullOrEmpty(storedToken)) { var refreshToken = await _localStorage.GetItemAsStringAsync("refreshToken"); var ticksStr = await _localStorage.GetItemAsStringAsync("tokenExpiry"); if (long.TryParse(ticksStr, out var ticks)) { _tokenStore.AccessToken = storedToken; _tokenStore.RefreshToken = refreshToken; _tokenStore.TokenExpiryTicks = ticks; accessToken = storedToken; } } } if (string.IsNullOrEmpty(_tokenStore.AccessToken)) { return new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity())); } // 토큰이 만료되면 로그아웃 if (_tokenStore.IsAccessTokenExpired()) { _logger.LogWarning("Access token 만료됨 - 자동 로그아웃"); await LogoutAsync(); return new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity())); } // 토큰이 5분 이내로 만료되면 자동 갱신 시도 (사용자 경험 향상) if (!string.IsNullOrEmpty(_tokenStore.RefreshToken) && ShouldRefreshToken()) { _logger.LogInformation("토큰 만료 5분 전 - 자동 갱신 시작"); var newTokenPair = await _authService.RefreshAccessTokenAsync(_tokenStore.RefreshToken); if (newTokenPair != null) { await LoginAsync(newTokenPair.AccessToken, newTokenPair.RefreshToken, newTokenPair.ExpiresIn); _logger.LogInformation("토큰 자동 갱신 성공"); accessToken = newTokenPair.AccessToken; } else { _logger.LogWarning("토큰 자동 갱신 실패 - 로그아웃"); await LogoutAsync(); return new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity())); } } var principal = _authService.ValidateToken(accessToken!); if (principal == null) { await LogoutAsync(); return new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity())); } return new AuthenticationState(principal); } catch (Exception ex) { _logger.LogError(ex, "인증 상태 조회 중 오류 발생"); return new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity())); } } public async Task LoginAsync(string accessToken, string refreshToken, int expiresIn) { var tokenExpiryTicks = DateTime.UtcNow.AddSeconds(expiresIn).Ticks; // TokenStore에 저장 (DelegatingHandler에서 사용) _tokenStore.AccessToken = accessToken; _tokenStore.RefreshToken = refreshToken; _tokenStore.TokenExpiryTicks = tokenExpiryTicks; // localStorage에도 저장 (페이지 리로드 후 복원) await _localStorage.SetItemAsStringAsync("accessToken", accessToken); await _localStorage.SetItemAsStringAsync("refreshToken", refreshToken); await _localStorage.SetItemAsStringAsync("tokenExpiry", tokenExpiryTicks.ToString()); NotifyAuthenticationStateChanged(GetAuthenticationStateAsync()); } private bool ShouldRefreshToken() { // 토큰이 5분 이내로 만료되면 갱신 (300초 = 5분) var tokenExpiryTicks = _tokenStore.TokenExpiryTicks; if (tokenExpiryTicks is null || tokenExpiryTicks <= 0) return false; const int refreshThresholdSeconds = 300; try { var expiryTime = new DateTime(tokenExpiryTicks.Value, DateTimeKind.Utc); var timeUntilExpiry = expiryTime - DateTime.UtcNow; return timeUntilExpiry.TotalSeconds <= refreshThresholdSeconds && timeUntilExpiry.TotalSeconds > 0; } catch { return false; } } public async Task LogoutAsync() { // TokenStore 초기화 _tokenStore.Clear(); // localStorage 초기화 await _localStorage.RemoveItemAsync("accessToken"); await _localStorage.RemoveItemAsync("refreshToken"); await _localStorage.RemoveItemAsync("tokenExpiry"); NotifyAuthenticationStateChanged(GetAuthenticationStateAsync()); } }