[] 로그인 동작 및 Repository 동작

1. 쿠키만 사용하던 저장 동작을 다른 저장소도 사용하게 변경
1.1. 쿠키 대신 세션 레포지토리를 기본적으로 사용하게 Service 코드 구현
2. 로그인 되었을 경우 화면 표기 변경
2.1. 시작하기 버튼 hidden 처리
2.2. 사용자 이름 불러오기
2.3. 로그인 동작 관련 변수 스토리지 저장
2.4. 서버에서 직접적인 크라이언트 쿠키 저장이 아닌 서버는 뒤의 값으로 간섭하게 변경
This commit is contained in:
SEAN-59 2025-06-11 15:12:22 +09:00
parent 3ffec93958
commit 9621169d57
15 changed files with 471 additions and 61 deletions

View File

@ -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<APIHeader, AppHeader>(
"/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);
}
}
}

View File

@ -4,12 +4,15 @@ using Microsoft.Extensions.Configuration;
using Front;
using Front.Program.Services;
var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add<App>("#app");
builder.RootComponents.Add<HeadOutlet>("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<APIService>();
builder.Services.AddScoped<CookieService>();
builder.Services.AddScoped<SecureService>();
builder.Services.AddScoped<StorageService>();
// builder.Services.AddRazorPages();
// builder.Services.AddServerSideBlazor();
builder.Services.AddScoped<LoadingService>();

View File

@ -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()

View File

@ -28,4 +28,15 @@ public class APIService
var response = await _http.GetFromJsonAsync<APIResponseStatus<TResponse>>($"{url}?{parameter}");
return response;
}
}
}
/*
dynamic :
Register.razor.cs에서 JsonElement를 . :
JSON (TryGetProperty )
*/

View File

@ -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<string?> GetCookieAsync(string key)
{
return await _js.InvokeAsync<string>("getCookie", key);
}
public async Task DeleteCookieAsync(string key)
{
await _js.InvokeVoidAsync("deleteCookie", key);
}
}

View File

@ -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<SecureService> _logger;
private readonly IJSRuntime _jsRuntime;
private readonly IWebAssemblyHostEnvironment _environment;
private string? _key;
private string? _iv;
private Task? _initializationTask;
public SecureService(
ILogger<SecureService> 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<JsonElement>("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<string> EncryptAsync(string plainText)
{
await EnsureInitializedAsync();
if (_key == null || _iv == null)
throw new InvalidOperationException("암호화 키가 초기화되지 않았습니다.");
try
{
return await _jsRuntime.InvokeAsync<string>("encryptText", plainText, _key, _iv);
}
catch (Exception ex)
{
_logger.LogError($"암호화 중 오류 발생: {ex.Message}");
throw;
}
}
public async Task<string> DecryptAsync(string cipherText)
{
await EnsureInitializedAsync();
if (_key == null || _iv == null)
throw new InvalidOperationException("암호화 키가 초기화되지 않았습니다.");
try
{
return await _jsRuntime.InvokeAsync<string>("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();
}

View File

@ -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<string?> GetItemAsync(string key, STORAGE_TYPE type = STORAGE_TYPE.Session)
{
return type switch
{
STORAGE_TYPE.Cookie => await _js.InvokeAsync<string>("getCookie", key),
STORAGE_TYPE.Local => await _js.InvokeAsync<string>("localStorage.getItem", key),
STORAGE_TYPE.Session => await _js.InvokeAsync<string>("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. .
*/

View File

@ -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<string>("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<string>("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))

View File

@ -11,7 +11,7 @@
<a class="text-text-title font-medium leading-normal hover:text-blue-600" href="/join" @onclick="() => isOpen = !isOpen">Join</a>
<a class="text-text-title font-medium leading-normal hover:text-blue-600" href="/new" @onclick="() => isOpen = !isOpen">What's New</a>
</div>
@if (!isLoggedIn)
@if (!isLogin)
{
<button class="flex min-w-[84px] max-w-[480px] cursor-pointer items-center justify-center overflow-hidden rounded-lg h-10 px-4 bg-second-normal hover:bg-second-dark text-white text-sm font-bold leading-normal tracking-[0.015em] mr-4"
@onclick="OnClickLogin">
@ -38,7 +38,7 @@
<a class="block w-full gap-y-2 text-text-title font-medium leading-normal hover:text-blue-600" href="/about" @onclick="() => isOpen = !isOpen">About</a>
<a class="block w-full gap-y-2 text-text-title font-medium leading-normal hover:text-blue-600" href="/join" @onclick="() => isOpen = !isOpen">Join</a>
<a class="block w-full gap-y-2 text-text-title font-medium leading-normal hover:text-blue-600" href="/new" @onclick="() => isOpen = !isOpen">What's New</a>
@if (!isLoggedIn)
@if (!isLogin)
{
<button class="flex w-full cursor-pointer items-center justify-center rounded-lg h-10 px-4 bg-second-normal hover:bg-second-dark text-white text-sm font-bold leading-normal tracking-[0.015em]"
@onclick="OnClickLogin">

View File

@ -1,24 +1,123 @@
using Microsoft.AspNetCore.Components;
using Microsoft.JSInterop;
using System;
using System.Net.Http.Json;
using System.Text.Json;
using Front.Program.Services;
namespace Front.Program.Views.Project;
public partial class TopProjectNav : ComponentBase
{
[Inject]
NavigationManager NavigationManager { get; set; } = default!;
[Inject] NavigationManager NavigationManager { get; set; } = default!;
[Inject] StorageService StorageService { get; set; } = default!;
[Inject] SecureService SecureService { get; set; } = default!;
[Inject]
IJSRuntime JS { get; set; } = default!;
[Inject] IJSRuntime JS { get; set; } = default!;
[Inject] HttpClient Http { get; set; } = default!;
string UserName { get; set; } = default!;
protected bool isOpen = false;
protected bool isLoggedIn = false;
bool isLogin = false;
protected override async Task OnInitializedAsync()
{
// 쿠키에서 로그인 상태 확인
var isLoginCookie = await JS.InvokeAsync<string>("eval", "document.cookie.split('; ').find(row => row.startsWith('IsLogin='))?.split('=')[1]");
isLoggedIn = isLoginCookie == "true";
Console.WriteLine("TopNAV 확인");
isLogin = isLogin = bool.TryParse(await StorageService.GetItemAsync("IsLogin"), out var result) && result;
if (isLogin) return;
try
{
var encryptedName = await StorageService.GetItemAsync("USER");
Console.WriteLine($"{encryptedName}");
if (!string.IsNullOrEmpty(encryptedName))
{
try
{
UserName = await SecureService.DecryptAsync(encryptedName);
Console.WriteLine($"세션 스토리지에서 가져온 사용자 이름: '{UserName}'");
return;
}
catch (Exception ex)
{
Console.WriteLine($"이름 복호화 중 오류 발생: {ex.Message}");
await StorageService.RemoveItemAsync("USER");
}
}
// apiSender.js의 함수를 사용하여 연결 키 가져오기
var headerValue = await StorageService.GetItemAsync("Web_AM_Connect_Key");
Console.WriteLine($"세션 스토리지에서 가져온 헤더 값: '{headerValue}'");
if (string.IsNullOrEmpty(headerValue))
{
Console.WriteLine("연결 키가 없습니다");
return;
}
// apiSender.js의 함수를 사용하여 API 호출
Console.WriteLine("세션 API 호출 시작");
var response = await JS.InvokeAsync<JsonElement>("fetchWithHeaderAndReturnUrl",
"/api/v1/in/user/auth/session",
"GET",
"Web_AM_Connect_Key",
headerValue);
Console.WriteLine($"세션 API 응답 타입: {response.ValueKind}");
Console.WriteLine($"세션 API 응답 내용: {response}");
if (response.ValueKind == JsonValueKind.Null || response.ValueKind == JsonValueKind.Undefined)
{
Console.WriteLine("응답이 null이거나 undefined입니다");
return;
}
try
{
if (response.TryGetProperty("status", out var statusElement))
{
Console.WriteLine($"status 요소 타입: {statusElement.ValueKind}");
if (statusElement.TryGetProperty("code", out var codeElement))
{
var code = codeElement.GetString();
Console.WriteLine($"응답 코드: {code}");
if (code == "000")
{
if (response.TryGetProperty("data", out var dataElement))
{
Console.WriteLine($"data 요소 타입: {dataElement.ValueKind}");
if (dataElement.TryGetProperty("name", out var nameElement))
{
UserName = nameElement.GetString() ?? "이름 없음";
isLogin = true;
Console.WriteLine($"NM: {UserName}");
var encryptedUserName = await SecureService.EncryptAsync(UserName);
Console.WriteLine($"NM: {encryptedUserName}");
await StorageService.SetItemAsync("USER", encryptedUserName);
await StorageService.SetItemAsync("Web_AM_Connect_Key", headerValue);
Console.WriteLine($"로그인된 사용자: {UserName}");
return;
}
}
}
}
}
Console.WriteLine("로그인되지 않은 상태");
}
catch (Exception ex)
{
Console.WriteLine($"응답 처리 중 오류 발생: {ex.Message}");
Console.WriteLine($"응답 처리 스택 트레이스: {ex.StackTrace}");
}
}
catch (Exception ex)
{
Console.WriteLine($"세션 확인 중 오류 발생: {ex.Message}");
Console.WriteLine($"스택 트레이스: {ex.StackTrace}");
}
}
public void OnClickMenuDown()

13
appsettings.json Normal file
View File

@ -0,0 +1,13 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*",
"Security": {
"EncryptionKey": "AcaMate2025SecureKeySeanForEncryption19940509",
"EncryptionIV": "AcaMate2025IV9459"
}
}

View File

@ -1,3 +1,7 @@
{
"ApiBaseUrl": "https://devacamate.ipstein.myds.me/"
"ApiBaseUrl": "https://devacamate.ipstein.myds.me/",
"Security": {
"EncryptionKey": "AcaMate2025SecureKeyForEncryptionSean1994",
"EncryptionIV": "AcaMate2025IV9459"
}
}

View File

@ -1,3 +1,7 @@
{
"ApiBaseUrl": "https://acamate.ipstein.myds.me/"
"ApiBaseUrl": "https://acamate.ipstein.myds.me/",
"Security": {
"EncryptionKey": "AcaMate2025SecureKeyForEncryptionSean1994",
"EncryptionIV": "AcaMate2025IV9459"
}
}

View File

@ -70,6 +70,7 @@
const v = document.cookie.match('(^|;)\\s*' + name + '\\s*=\\s*([^;]+)');
return v ? v.pop() : '';
};
window.setCookie = function (name, value, days) {
let expires = "";
if (days) {

View File

@ -12,12 +12,85 @@ window.postWithHeader = function(url, method, headerKey, headerValue) {
};
window.fetchWithHeaderAndReturnUrl = async function(url, method, headerKey, headerValue) {
const response = await fetch(url, {
method: method,
headers: {
[headerKey]: headerValue
try {
const response = await fetch(url, {
method: method,
headers: {
[headerKey]: headerValue
}
});
if (!response.ok) {
console.error('API 호출 실패:', response.status, response.statusText);
return null;
}
});
const data = await response.json();
return data.url;
};
const data = await response.json();
console.log('API 응답 데이터:', data);
return data;
} catch (error) {
console.error('API 호출 중 오류 발생:', error);
return null;
}
};
window.loadConfig = async function(configFile) {
try {
console.log('설정 파일 로드 시도:', configFile);
const response = await fetch(configFile);
if (!response.ok) {
console.error('설정 파일 로드 실패:', response.status, response.statusText);
return null;
}
const config = await response.json();
console.log('설정 파일 로드 성공:', configFile);
return config;
} catch (error) {
console.error('설정 파일 로드 중 오류 발생:', error);
return null;
}
};
window.encryptText = async function(text, key, iv) {
try {
// XOR 암호화 구현
let result = '';
for (let i = 0; i < text.length; i++) {
const charCode = text.charCodeAt(i) ^ key.charCodeAt(i % key.length);
result += String.fromCharCode(charCode);
}
// UTF-8로 인코딩 후 Base64로 변환
const utf8Encoder = new TextEncoder();
const bytes = utf8Encoder.encode(result);
const base64 = btoa(String.fromCharCode.apply(null, bytes));
return base64;
} catch (error) {
console.error('암호화 중 오류 발생:', error);
throw error;
}
};
window.decryptText = async function(encryptedText, key, iv) {
try {
// Base64 디코딩 후 UTF-8 디코딩
const binary = atob(encryptedText);
const bytes = new Uint8Array(binary.length);
for (let i = 0; i < binary.length; i++) {
bytes[i] = binary.charCodeAt(i);
}
const utf8Decoder = new TextDecoder();
const text = utf8Decoder.decode(bytes);
// XOR 복호화
let result = '';
for (let i = 0; i < text.length; i++) {
const charCode = text.charCodeAt(i) ^ key.charCodeAt(i % key.length);
result += String.fromCharCode(charCode);
}
return result;
} catch (error) {
console.error('복호화 중 오류 발생:', error);
throw error;
}
};