From 9621169d57de7cbc3d038ecc45fb02010dc50b24 Mon Sep 17 00:00:00 2001 From: SEAN-59 Date: Wed, 11 Jun 2025 15:12:22 +0900 Subject: [PATCH] =?UTF-8?q?[=E2=9C=A8]=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20?= =?UTF-8?q?=EB=8F=99=EC=9E=91=20=EB=B0=8F=20Repository=20=EB=8F=99?= =?UTF-8?q?=EC=9E=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 쿠키만 사용하던 저장 동작을 다른 저장소도 사용하게 변경 1.1. 쿠키 대신 세션 레포지토리를 기본적으로 사용하게 Service 코드 구현 2. 로그인 되었을 경우 화면 표기 변경 2.1. 시작하기 버튼 hidden 처리 2.2. 사용자 이름 불러오기 2.3. 로그인 동작 관련 변수 스토리지 저장 2.4. 서버에서 직접적인 크라이언트 쿠키 저장이 아닌 서버는 뒤의 값으로 간섭하게 변경 --- App.razor.cs | 8 +- Program.cs | 8 +- Program/Layout/MainLayout.razor.cs | 38 +++++- Program/Services/APIService.cs | 13 +- Program/Services/CookieService.cs | 29 ----- Program/Services/SecureService.cs | 120 +++++++++++++++++++ Program/Services/StorageService.cs | 75 ++++++++++++ Program/Views/Project/Register.razor.cs | 7 +- Program/Views/Project/TopProjectNav.razor | 4 +- Program/Views/Project/TopProjectNav.razor.cs | 115 ++++++++++++++++-- appsettings.json | 13 ++ wwwroot/appsettings.Development.json | 6 +- wwwroot/appsettings.Production.json | 6 +- wwwroot/index.html | 1 + wwwroot/scripts/apiSender.js | 89 ++++++++++++-- 15 files changed, 471 insertions(+), 61 deletions(-) delete mode 100644 Program/Services/CookieService.cs create mode 100644 Program/Services/SecureService.cs create mode 100644 Program/Services/StorageService.cs create mode 100644 appsettings.json 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) {