diff --git a/App.razor.cs b/App.razor.cs index b39d420..055c043 100644 --- a/App.razor.cs +++ b/App.razor.cs @@ -8,17 +8,17 @@ namespace Front; public partial class App : ComponentBase { [Inject] private APIService API { get; set; } = default!; - [Inject] private CookieService Cookie { get; set; } = default!; + [Inject] private StorageService StorageService { get; set; } = default!; protected override async Task OnAfterRenderAsync(bool firstRender) { if (firstRender) { - var cookie = await Cookie.GetCookieAsync("Web_AM_Connect_Key"); + var headerValue = await StorageService.GetItemAsync("Web_AM_Connect_Key"); // 값 없으면 API 호출 - if (string.IsNullOrEmpty(cookie)) + if (string.IsNullOrEmpty(headerValue)) { var response = await API.GetJsonAsync( "/api/v1/in/app", @@ -30,7 +30,7 @@ public partial class App : ComponentBase }); if (!string.IsNullOrEmpty(response.data.header)) { - await Cookie.SetCookieAsync("Web_AM_Connect_Key", response.data.header); + await StorageService.SetItemAsync("Web_AM_Connect_Key", response.data.header); } } } diff --git a/Program.cs b/Program.cs index 8e520df..5ad3930 100644 --- a/Program.cs +++ b/Program.cs @@ -4,12 +4,15 @@ using Microsoft.Extensions.Configuration; using Front; using Front.Program.Services; - + var builder = WebAssemblyHostBuilder.CreateDefault(args); builder.RootComponents.Add("#app"); builder.RootComponents.Add("head::after"); +// 설정 파일 로드 +// builder.Configuration.AddJsonFile("appsettings.json", optional: false); +builder.Configuration.AddJsonFile($"appsettings.{builder.HostEnvironment.Environment}.json", optional: true); builder.Services.AddScoped(sp => //new HttpClient { @@ -30,7 +33,8 @@ builder.Services.AddScoped(sp => //new HttpClient // SCOPED 으로 등록된 서비스는 DI 컨테이너에 등록된 서비스의 인스턴스를 사용합니다. builder.Services.AddScoped(); -builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); // builder.Services.AddRazorPages(); // builder.Services.AddServerSideBlazor(); builder.Services.AddScoped(); diff --git a/Program/Layout/MainLayout.razor.cs b/Program/Layout/MainLayout.razor.cs index 4f8cb25..cdfeb2a 100644 --- a/Program/Layout/MainLayout.razor.cs +++ b/Program/Layout/MainLayout.razor.cs @@ -1,5 +1,6 @@ using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components.Routing; + using Front.Program.Views.Project; using Front.Program.Services; @@ -13,6 +14,9 @@ public partial class MainLayout : LayoutComponentBase, IDisposable [Inject] LoadingService LoadingService { get; set; } = default!; + [Inject] + StorageService StorageService { get; set; } = default!; + // 경로의 시작 부분 // protected bool isHidePrjTop => Navigation.ToBaseRelativePath(Navigation.Uri).StartsWith("auth", StringComparison.OrdinalIgnoreCase); @@ -23,11 +27,43 @@ public partial class MainLayout : LayoutComponentBase, IDisposable { LoadingService.OnChange += StateHasChanged; Navigation.LocationChanged += HandleLocationChanged; + HandleLocationChanged(this, new LocationChangedEventArgs(Navigation.Uri, false)); } - private void HandleLocationChanged(object? sender, LocationChangedEventArgs e) + private async void HandleLocationChanged(object? sender, LocationChangedEventArgs e) { LoadingService.HideNavigationLoading(); + + var uri = Navigation.ToAbsoluteUri(Navigation.Uri); + Console.WriteLine($"리다이렉트된 URI: {uri}"); + + if (uri.Query.Contains("auth=")) + { + var query = uri.Query.TrimStart('?'); + var parameters = query.Split('&') + .Select(p => p.Split('=')) + .Where(p => p.Length == 2) + .ToDictionary(p => p[0], p => p[1]); + + if (parameters.TryGetValue("auth", out var auth)) + { + Console.WriteLine($"auth 파라미터 값: {auth}"); + if (auth == "true") + { + await StorageService.SetItemAsync("IsLogin", "true"); + Console.WriteLine("로그인 상태를 true로 설정했습니다."); + } + else + { + await StorageService.RemoveItemAsync("IsLogin"); + Console.WriteLine("로그인 상태를 제거했습니다."); + } + + // 파라미터를 제거하고 리다이렉트 + var baseUri = uri.GetLeftPart(UriPartial.Path); + Navigation.NavigateTo(baseUri, forceLoad: false); + } + } } public void Dispose() diff --git a/Program/Services/APIService.cs b/Program/Services/APIService.cs index 51f3845..861d64e 100644 --- a/Program/Services/APIService.cs +++ b/Program/Services/APIService.cs @@ -28,4 +28,15 @@ public class APIService var response = await _http.GetFromJsonAsync>($"{url}?{parameter}"); return response; } -} \ No newline at end of file +} + +/* +dynamic 타입을 사용하면: +타입 안전성이 떨어집니다 +컴파일 타임에 오류를 잡을 수 없습니다 +런타임에 예상치 못한 오류가 발생할 수 있습니다 +현재 Register.razor.cs에서 JsonElement를 사용하는 방식이 더 안전할 수 있습니다. 왜냐하면: +JSON 응답의 구조를 명시적으로 확인할 수 있습니다 (TryGetProperty 사용) +각 속성의 타입을 명확하게 처리할 수 있습니다 +예상치 못한 데이터 구조에 대해 더 안전하게 대응할 수 있습니다 +*/ \ No newline at end of file diff --git a/Program/Services/CookieService.cs b/Program/Services/CookieService.cs deleted file mode 100644 index 5fc16ba..0000000 --- a/Program/Services/CookieService.cs +++ /dev/null @@ -1,29 +0,0 @@ -using Microsoft.AspNetCore.Components; -using Microsoft.JSInterop; - -namespace Front.Program.Services; - -public class CookieService -{ - private readonly IJSRuntime _js; - - public CookieService(IJSRuntime js) - { - _js = js; - } - - public async Task SetCookieAsync(string key, string value) - { - await _js.InvokeVoidAsync("setCookie", key, value, 1); - } - - public async Task GetCookieAsync(string key) - { - return await _js.InvokeAsync("getCookie", key); - } - - public async Task DeleteCookieAsync(string key) - { - await _js.InvokeVoidAsync("deleteCookie", key); - } -} \ No newline at end of file diff --git a/Program/Services/SecureService.cs b/Program/Services/SecureService.cs new file mode 100644 index 0000000..42977f2 --- /dev/null +++ b/Program/Services/SecureService.cs @@ -0,0 +1,120 @@ +using System.Security.Cryptography; +using System.Text; +using Microsoft.JSInterop; +using System.Text.Json; +using Microsoft.AspNetCore.Components.WebAssembly.Hosting; + +namespace Front.Program.Services; + +public class SecureService +{ + private readonly ILogger _logger; + private readonly IJSRuntime _jsRuntime; + private readonly IWebAssemblyHostEnvironment _environment; + private string? _key; + private string? _iv; + private Task? _initializationTask; + + public SecureService( + ILogger logger, + IJSRuntime jsRuntime, + IWebAssemblyHostEnvironment environment) + { + _logger = logger; + _jsRuntime = jsRuntime; + _environment = environment; + _initializationTask = InitializeAsync(); + } + + private async Task InitializeAsync() + { + try + { + var configFile = $"appsettings.{_environment.Environment}.json"; + _logger.LogInformation($"설정 파일 로드: {configFile}"); + + var config = await _jsRuntime.InvokeAsync("loadConfig", configFile); + if (config.ValueKind == JsonValueKind.Null) + { + throw new InvalidOperationException($"설정 파일을 로드할 수 없습니다: {configFile}"); + } + + var security = config.GetProperty("Security"); + _key = security.GetProperty("EncryptionKey").GetString() + ?? throw new ArgumentNullException("Security:EncryptionKey"); + _iv = security.GetProperty("EncryptionIV").GetString() + ?? throw new ArgumentNullException("Security:EncryptionIV"); + } + catch (Exception ex) + { + _logger.LogError($"설정 로드 중 오류 발생: {ex.Message}"); + throw; + } + } + + private async Task EnsureInitializedAsync() + { + if (_initializationTask != null) + { + await _initializationTask; + _initializationTask = null; + } + } + + private byte[] GetKeyBytes(string key) + { + using (var sha256 = SHA256.Create()) + { + return sha256.ComputeHash(Encoding.UTF8.GetBytes(key)); + } + } + + private byte[] GetIVBytes(string iv) + { + using (var sha256 = SHA256.Create()) + { + var hash = sha256.ComputeHash(Encoding.UTF8.GetBytes(iv)); + return hash.Take(16).ToArray(); // IV는 16바이트만 필요 + } + } + + public async Task EncryptAsync(string plainText) + { + await EnsureInitializedAsync(); + + if (_key == null || _iv == null) + throw new InvalidOperationException("암호화 키가 초기화되지 않았습니다."); + + try + { + return await _jsRuntime.InvokeAsync("encryptText", plainText, _key, _iv); + } + catch (Exception ex) + { + _logger.LogError($"암호화 중 오류 발생: {ex.Message}"); + throw; + } + } + + public async Task DecryptAsync(string cipherText) + { + await EnsureInitializedAsync(); + + if (_key == null || _iv == null) + throw new InvalidOperationException("암호화 키가 초기화되지 않았습니다."); + + try + { + return await _jsRuntime.InvokeAsync("decryptText", cipherText, _key, _iv); + } + catch (Exception ex) + { + _logger.LogError($"복호화 중 오류 발생: {ex.Message}"); + throw; + } + } + + // 동기 메서드는 비동기 메서드를 호출하도록 수정 + public string Encrypt(string plainText) => EncryptAsync(plainText).GetAwaiter().GetResult(); + public string Decrypt(string cipherText) => DecryptAsync(cipherText).GetAwaiter().GetResult(); +} \ No newline at end of file diff --git a/Program/Services/StorageService.cs b/Program/Services/StorageService.cs new file mode 100644 index 0000000..5817ac9 --- /dev/null +++ b/Program/Services/StorageService.cs @@ -0,0 +1,75 @@ +using Microsoft.AspNetCore.Components; +using Microsoft.JSInterop; + +namespace Front.Program.Services; + +public enum STORAGE_TYPE +{ + Cookie, + Local, + Session +} + +public class StorageService +{ + private readonly IJSRuntime _js; + + public StorageService(IJSRuntime js) + { + _js = js; + } + + public async Task SetItemAsync(string key, string value, STORAGE_TYPE type = STORAGE_TYPE.Session, int expire_time = 1) + { + await (type switch + { + STORAGE_TYPE.Cookie => _js.InvokeVoidAsync("setCookie", key, value, expire_time), + STORAGE_TYPE.Local => _js.InvokeVoidAsync("localStorage.setItem", key, value), + STORAGE_TYPE.Session => _js.InvokeVoidAsync("sessionStorage.setItem", key, value), + _ => throw new ArgumentException("Invalid storage type") + }); + } + + public async Task GetItemAsync(string key, STORAGE_TYPE type = STORAGE_TYPE.Session) + { + return type switch + { + STORAGE_TYPE.Cookie => await _js.InvokeAsync("getCookie", key), + STORAGE_TYPE.Local => await _js.InvokeAsync("localStorage.getItem", key), + STORAGE_TYPE.Session => await _js.InvokeAsync("sessionStorage.getItem", key), + _ => null + }; + } + + public async Task RemoveItemAsync(string key, STORAGE_TYPE type = STORAGE_TYPE.Session) + { + await (type switch + { + STORAGE_TYPE.Cookie => _js.InvokeVoidAsync("deleteCookie", key), + STORAGE_TYPE.Local => _js.InvokeVoidAsync("localStorage.removeItem", key), + STORAGE_TYPE.Session => _js.InvokeVoidAsync("sessionStorage.removeItem", key), + _ => throw new ArgumentException("Invalid storage type") + }); + } +} + +/* 스토리지 종류 +1. Cookie + 만료일 : 명시적으로 설정이 가능 (설정에 따라 그 값을 변경 가능) + 접근성 : 서버와 클라이언트 모두 접근이 가능함 + 특징 + 1. HTTP 요청마다 서버로 전송이 되며 도메인/경로 제한이 가능하다. + 2. HttpOnly나 secure 옵션으로 보안 설정이 가능하다. +2. Local + 만료일 : 브라우저 데이터 삭제 전까지는 영구 보관 가능 + 접근성 : 클라이언트 측에서 JS 를 통해서만 접근이 가능 + 특징 + 1. 도메인별로 분리된 저장소로 동기적인 동작이 된다. + 2. 문자열만 저장이 가능하다. +3. Sesson + 만료일 : 브라우저의 탭/창 닫을 때 까지 보관 (닫게 되면 삭제) + 접근성 : 클라이언트 측에서 JS 를 통해서만 접근이 가능 + 특징 + 1. 탭/창 별로 불리된 저장소며 동기적인 동작이 가능하다. + 2. 문자열만 저장이 가능하다. + */ diff --git a/Program/Views/Project/Register.razor.cs b/Program/Views/Project/Register.razor.cs index 903cdce..7418326 100644 --- a/Program/Views/Project/Register.razor.cs +++ b/Program/Views/Project/Register.razor.cs @@ -16,6 +16,7 @@ public partial class Register : ComponentBase [Inject] private HttpClient Http { get; set; } = default!; [Inject] private IConfiguration Configuration { get; set; } = default!; [Inject] private LoadingService LoadingService { get; set; } = default!; + [Inject] private StorageService CookieService { get; set; } = default!; private ElementReference dateInputRef; @@ -39,8 +40,7 @@ public partial class Register : ComponentBase objRef = DotNetObjectReference.Create(this); try { - // 쿠키에서 토큰 가져오기 - var token = await JS.InvokeAsync("eval", "document.cookie.split('; ').find(row => row.startsWith('Web_AM_Connect_Key='))?.split('=')[1]"); + var token = await CookieService.GetItemAsync("Web_AM_Connect_Key"); if (string.IsNullOrEmpty(token)) { await JS.InvokeVoidAsync("alert", "인증 정보가 없습니다."); @@ -169,8 +169,7 @@ public partial class Register : ComponentBase try { LoadingService.ShowLoading(); - // 쿠키에서 토큰 가져오기 - var token = await JS.InvokeAsync("eval", "document.cookie.split('; ').find(row => row.startsWith('Web_AM_Connect_Key='))?.split('=')[1] || ''"); + var token = await CookieService.GetItemAsync("Web_AM_Connect_Key"); Console.WriteLine($"쿠키에서 가져온 토큰: '{token}'"); if (string.IsNullOrEmpty(token)) diff --git a/Program/Views/Project/TopProjectNav.razor b/Program/Views/Project/TopProjectNav.razor index 00d8958..e9954be 100644 --- a/Program/Views/Project/TopProjectNav.razor +++ b/Program/Views/Project/TopProjectNav.razor @@ -11,7 +11,7 @@ Join What's New - @if (!isLoggedIn) + @if (!isLogin) {