diff --git a/TaxBaik.Admin/Components/Pages/Login.razor b/TaxBaik.Admin/Components/Pages/Login.razor
index 938ea95..e259353 100644
--- a/TaxBaik.Admin/Components/Pages/Login.razor
+++ b/TaxBaik.Admin/Components/Pages/Login.razor
@@ -1,9 +1,10 @@
@page "/login"
@using System.ComponentModel.DataAnnotations
-@using Microsoft.AspNetCore.Authentication
-@using Microsoft.AspNetCore.Authentication.Cookies
@layout TaxBaik.Admin.Components.Layout.BlankLayout
@attribute [AllowAnonymous]
+@inject AuthService AuthService
+@inject NavigationManager NavigationManager
+@inject CustomAuthenticationStateProvider AuthStateProvider
로그인
@@ -24,7 +25,17 @@
}
로그인
+ Size="Size.Large" OnClick="HandleLogin" Disabled="isLoading">
+ @if (isLoading)
+ {
+
+ 로그인 중...
+ }
+ else
+ {
+ 로그인
+ }
+
@@ -32,30 +43,43 @@
@code {
private MudForm form;
private bool isFormValid = false;
+ private bool isLoading = false;
private string errorMessage = "";
private LoginModel model = new();
private async Task HandleLogin()
{
- // 기본 사용자명: admin / 비밀번호: admin123
- if (model.Username == "admin" && model.Password == "admin123")
+ if (isLoading)
+ return;
+
+ isLoading = true;
+ errorMessage = "";
+
+ try
{
- // 임시: 대시보드로 리다이렉트 (향후 실제 쿠키 인증으로 개선)
- NavigationManager.NavigateTo("/taxbaik/admin/dashboard", forceLoad: true);
+ var token = await AuthService.AuthenticateAndGenerateTokenAsync(model.Username, model.Password);
+
+ if (token == null)
+ {
+ errorMessage = "사용자명 또는 비밀번호가 올바르지 않습니다.";
+ isLoading = false;
+ return;
+ }
+
+ await AuthStateProvider.LoginAsync(token);
+ NavigationManager.NavigateTo("/taxbaik/admin/dashboard", forceLoad: false);
}
- else
+ catch (Exception ex)
{
- errorMessage = "사용자명 또는 비밀번호가 올바르지 않습니다.";
+ errorMessage = "로그인 중 오류가 발생했습니다.";
+ isLoading = false;
}
}
- [Inject]
- private NavigationManager NavigationManager { get; set; }
-
private class LoginModel
{
- public string Username { get; set; }
- public string Password { get; set; }
+ public string Username { get; set; } = "";
+ public string Password { get; set; } = "";
}
}
diff --git a/TaxBaik.Admin/Components/Routes.razor b/TaxBaik.Admin/Components/Routes.razor
index 4db38c6..b54245a 100644
--- a/TaxBaik.Admin/Components/Routes.razor
+++ b/TaxBaik.Admin/Components/Routes.razor
@@ -1,14 +1,17 @@
@using Microsoft.AspNetCore.Components.Routing
+@using Microsoft.AspNetCore.Components.Authorization
-
-
-
-
-
-
- 찾을 수 없음
-
- 요청한 페이지를 찾을 수 없습니다.
-
-
-
+
+
+
+
+
+
+
+ 찾을 수 없음
+
+ 요청한 페이지를 찾을 수 없습니다.
+
+
+
+
diff --git a/TaxBaik.Admin/Components/_Imports.razor b/TaxBaik.Admin/Components/_Imports.razor
index 50c33cc..1c588e0 100644
--- a/TaxBaik.Admin/Components/_Imports.razor
+++ b/TaxBaik.Admin/Components/_Imports.razor
@@ -7,3 +7,5 @@
@using Microsoft.AspNetCore.Components.Authorization
@using Microsoft.AspNetCore.Authorization
@using MudBlazor
+@using TaxBaik.Admin.Services
+@attribute [Authorize]
diff --git a/TaxBaik.Admin/Program.cs b/TaxBaik.Admin/Program.cs
index c9d9d8f..f540d24 100644
--- a/TaxBaik.Admin/Program.cs
+++ b/TaxBaik.Admin/Program.cs
@@ -1,18 +1,18 @@
using System.Text.Encodings.Web;
using System.Text.Unicode;
-using Microsoft.AspNetCore.Authentication.Cookies;
+using Microsoft.AspNetCore.Components.Authorization;
using MudBlazor.Services;
+using TaxBaik.Admin.Services;
using TaxBaik.Application;
using TaxBaik.Infrastructure;
var builder = WebApplication.CreateBuilder(args);
-builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
- .AddCookie(opts => {
- opts.LoginPath = "/login";
- opts.ExpireTimeSpan = TimeSpan.FromHours(8);
- opts.Cookie.SameSite = SameSiteMode.Lax;
- });
+builder.Services.AddScoped();
+builder.Services.AddScoped();
+builder.Services.AddScoped(sp => sp.GetRequiredService());
+builder.Services.AddScoped();
+builder.Services.AddCascadingAuthenticationState();
builder.Services.AddAuthorizationCore();
builder.Services.AddRazorComponents()
diff --git a/TaxBaik.Admin/Services/AuthService.cs b/TaxBaik.Admin/Services/AuthService.cs
new file mode 100644
index 0000000..509ef4f
--- /dev/null
+++ b/TaxBaik.Admin/Services/AuthService.cs
@@ -0,0 +1,96 @@
+using System.IdentityModel.Tokens.Jwt;
+using System.Security.Claims;
+using System.Text;
+using BCrypt.Net;
+using Microsoft.IdentityModel.Tokens;
+using TaxBaik.Domain.Entities;
+using TaxBaik.Domain.Interfaces;
+
+namespace TaxBaik.Admin.Services;
+
+public class AuthService
+{
+ private readonly IAdminUserRepository _adminUserRepository;
+ private readonly ILogger _logger;
+ private readonly string _jwtSecretKey;
+ private readonly int _tokenExpirationMinutes = 480; // 8시간
+
+ public AuthService(IAdminUserRepository adminUserRepository, ILogger logger, IConfiguration configuration)
+ {
+ _adminUserRepository = adminUserRepository;
+ _logger = logger;
+ _jwtSecretKey = configuration["Jwt:SecretKey"] ?? throw new InvalidOperationException("Missing 'Jwt:SecretKey' configuration.");
+ }
+
+ public async Task AuthenticateAndGenerateTokenAsync(string username, string password)
+ {
+ if (string.IsNullOrWhiteSpace(username) || string.IsNullOrWhiteSpace(password))
+ {
+ return null;
+ }
+
+ var user = await _adminUserRepository.GetByUsernameAsync(username);
+ if (user == null)
+ {
+ _logger.LogWarning("로그인 시도: 존재하지 않는 사용자 '{Username}'", username);
+ return null;
+ }
+
+ if (!BCrypt.Net.BCrypt.Verify(password, user.PasswordHash))
+ {
+ _logger.LogWarning("로그인 시도: 잘못된 비밀번호 '{Username}'", username);
+ return null;
+ }
+
+ _logger.LogInformation("로그인 성공: {Username}", username);
+ return GenerateJwtToken(user);
+ }
+
+ private string GenerateJwtToken(AdminUser user)
+ {
+ var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_jwtSecretKey));
+ var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
+
+ var claims = new[]
+ {
+ new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()),
+ new Claim(ClaimTypes.Name, user.Username),
+ new Claim("username", user.Username)
+ };
+
+ var token = new JwtSecurityToken(
+ issuer: "taxbaik-admin",
+ audience: "taxbaik-admin-client",
+ claims: claims,
+ expires: DateTime.UtcNow.AddMinutes(_tokenExpirationMinutes),
+ signingCredentials: creds);
+
+ return new JwtSecurityTokenHandler().WriteToken(token);
+ }
+
+ public ClaimsPrincipal? ValidateToken(string token)
+ {
+ try
+ {
+ var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_jwtSecretKey));
+ var handler = new JwtSecurityTokenHandler();
+ var principal = handler.ValidateToken(token, new TokenValidationParameters
+ {
+ ValidateIssuerSigningKey = true,
+ IssuerSigningKey = key,
+ ValidateIssuer = true,
+ ValidIssuer = "taxbaik-admin",
+ ValidateAudience = true,
+ ValidAudience = "taxbaik-admin-client",
+ ValidateLifetime = true,
+ ClockSkew = TimeSpan.Zero
+ }, out SecurityToken validatedToken);
+
+ return principal;
+ }
+ catch
+ {
+ return null;
+ }
+ }
+}
diff --git a/TaxBaik.Admin/Services/CustomAuthenticationStateProvider.cs b/TaxBaik.Admin/Services/CustomAuthenticationStateProvider.cs
new file mode 100644
index 0000000..602a5c5
--- /dev/null
+++ b/TaxBaik.Admin/Services/CustomAuthenticationStateProvider.cs
@@ -0,0 +1,59 @@
+using System.Security.Claims;
+using Microsoft.AspNetCore.Components.Authorization;
+
+namespace TaxBaik.Admin.Services;
+
+public class CustomAuthenticationStateProvider : AuthenticationStateProvider
+{
+ private readonly ILocalStorageService _localStorage;
+ private readonly AuthService _authService;
+ private readonly ILogger _logger;
+
+ public CustomAuthenticationStateProvider(ILocalStorageService localStorage, AuthService authService, ILogger logger)
+ {
+ _localStorage = localStorage;
+ _authService = authService;
+ _logger = logger;
+ }
+
+ public override async Task GetAuthenticationStateAsync()
+ {
+ try
+ {
+ var token = await _localStorage.GetItemAsStringAsync("auth_token");
+
+ if (string.IsNullOrEmpty(token))
+ {
+ return new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity()));
+ }
+
+ var principal = _authService.ValidateToken(token);
+ if (principal == null)
+ {
+ await _localStorage.RemoveItemAsync("auth_token");
+ 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 token)
+ {
+ await _localStorage.SetItemAsStringAsync("auth_token", token);
+
+ var principal = _authService.ValidateToken(token);
+ NotifyAuthenticationStateChanged(GetAuthenticationStateAsync());
+ }
+
+ public async Task LogoutAsync()
+ {
+ await _localStorage.RemoveItemAsync("auth_token");
+ NotifyAuthenticationStateChanged(GetAuthenticationStateAsync());
+ }
+}
diff --git a/TaxBaik.Admin/Services/ILocalStorageService.cs b/TaxBaik.Admin/Services/ILocalStorageService.cs
new file mode 100644
index 0000000..342530c
--- /dev/null
+++ b/TaxBaik.Admin/Services/ILocalStorageService.cs
@@ -0,0 +1,8 @@
+namespace TaxBaik.Admin.Services;
+
+public interface ILocalStorageService
+{
+ Task GetItemAsStringAsync(string key);
+ Task SetItemAsStringAsync(string key, string value);
+ Task RemoveItemAsync(string key);
+}
diff --git a/TaxBaik.Admin/Services/LocalStorageService.cs b/TaxBaik.Admin/Services/LocalStorageService.cs
new file mode 100644
index 0000000..fa73188
--- /dev/null
+++ b/TaxBaik.Admin/Services/LocalStorageService.cs
@@ -0,0 +1,43 @@
+using Microsoft.JSInterop;
+
+namespace TaxBaik.Admin.Services;
+
+public class LocalStorageService : ILocalStorageService
+{
+ private readonly IJSRuntime _jsRuntime;
+
+ public LocalStorageService(IJSRuntime jsRuntime)
+ {
+ _jsRuntime = jsRuntime;
+ }
+
+ public async Task GetItemAsStringAsync(string key)
+ {
+ try
+ {
+ return await _jsRuntime.InvokeAsync("localStorage.getItem", key);
+ }
+ catch
+ {
+ return null;
+ }
+ }
+
+ public async Task SetItemAsStringAsync(string key, string value)
+ {
+ try
+ {
+ await _jsRuntime.InvokeVoidAsync("localStorage.setItem", key, value);
+ }
+ catch { }
+ }
+
+ public async Task RemoveItemAsync(string key)
+ {
+ try
+ {
+ await _jsRuntime.InvokeVoidAsync("localStorage.removeItem", key);
+ }
+ catch { }
+ }
+}
diff --git a/TaxBaik.Admin/TaxBaik.Admin.csproj b/TaxBaik.Admin/TaxBaik.Admin.csproj
index 8f8c88c..b7bea4b 100644
--- a/TaxBaik.Admin/TaxBaik.Admin.csproj
+++ b/TaxBaik.Admin/TaxBaik.Admin.csproj
@@ -14,6 +14,8 @@
+
+
diff --git a/TaxBaik.Admin/appsettings.json b/TaxBaik.Admin/appsettings.json
index 10f68b8..d965bed 100644
--- a/TaxBaik.Admin/appsettings.json
+++ b/TaxBaik.Admin/appsettings.json
@@ -5,5 +5,8 @@
"Microsoft.AspNetCore": "Warning"
}
},
- "AllowedHosts": "*"
+ "AllowedHosts": "*",
+ "Jwt": {
+ "SecretKey": "dev-secret-key-change-in-production-min-32-chars!"
+ }
}