This commit is contained in:
@@ -64,33 +64,3 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@if (Model.LoginSucceeded && Model.TokenPair is not null)
|
||||
{
|
||||
<script>
|
||||
(function () {
|
||||
const tokenPair = @Html.Raw(System.Text.Json.JsonSerializer.Serialize(
|
||||
Model.TokenPair,
|
||||
new System.Text.Json.JsonSerializerOptions { PropertyNamingPolicy = System.Text.Json.JsonNamingPolicy.CamelCase }));
|
||||
const expiryTicks = 621355968000000000 + ((Date.now() + (tokenPair.expiresIn || 3600) * 1000) * 10000);
|
||||
localStorage.setItem('accessToken', tokenPair.accessToken || '');
|
||||
localStorage.setItem('refreshToken', tokenPair.refreshToken || '');
|
||||
localStorage.setItem('tokenExpiry', String(expiryTicks));
|
||||
@if (Model.RememberMe)
|
||||
{
|
||||
<text>
|
||||
localStorage.setItem('admin-remembered-username', @Html.Raw(System.Text.Json.JsonSerializer.Serialize(Model.Username)));
|
||||
localStorage.setItem('admin-remember-checkbox', 'true');
|
||||
</text>
|
||||
}
|
||||
else
|
||||
{
|
||||
<text>
|
||||
localStorage.removeItem('admin-remembered-username');
|
||||
localStorage.removeItem('admin-remember-checkbox');
|
||||
</text>
|
||||
}
|
||||
|
||||
window.location.replace(@Html.Raw(System.Text.Json.JsonSerializer.Serialize(Model.RedirectUrl)));
|
||||
})();
|
||||
</script>
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Security.Claims;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
using TaxBaik.Web.Endpoints.Auth;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using TaxBaik.Web.Services;
|
||||
|
||||
namespace TaxBaik.Web.Pages.Admin;
|
||||
@@ -27,11 +28,6 @@ public class LoginModel(AuthService authService) : PageModel
|
||||
public string? ReturnUrl { get; set; }
|
||||
|
||||
public string? ErrorMessage { get; set; }
|
||||
|
||||
public bool LoginSucceeded { get; set; }
|
||||
|
||||
public TokenPairResponse? TokenPair { get; set; }
|
||||
|
||||
public string RedirectUrl { get; set; } = "/admin/dashboard";
|
||||
|
||||
public void OnGet()
|
||||
@@ -49,28 +45,32 @@ public class LoginModel(AuthService authService) : PageModel
|
||||
return Page();
|
||||
}
|
||||
|
||||
var authResult = await authService.AuthenticateAndGenerateTokenPairAsync(Username, Password);
|
||||
if (!authResult.IsSuccess || authResult.TokenPair is null)
|
||||
var user = await authService.AuthenticateAsync(Username, Password);
|
||||
if (user is null)
|
||||
{
|
||||
ErrorMessage = authResult.FailureReason switch
|
||||
{
|
||||
AuthFailureReason.DatabaseUnavailable => "로그인 서비스를 일시적으로 사용할 수 없습니다. 잠시 후 다시 시도하세요.",
|
||||
_ => "아이디 또는 비밀번호가 올바르지 않습니다."
|
||||
};
|
||||
ErrorMessage = "아이디 또는 비밀번호가 올바르지 않습니다.";
|
||||
return Page();
|
||||
}
|
||||
|
||||
LoginSucceeded = true;
|
||||
RedirectUrl = NormalizeRedirectUrl(ReturnUrl);
|
||||
TokenPair = new TokenPairResponse
|
||||
|
||||
var claims = new List<Claim>
|
||||
{
|
||||
AccessToken = authResult.TokenPair.AccessToken,
|
||||
RefreshToken = authResult.TokenPair.RefreshToken,
|
||||
ExpiresIn = authResult.TokenPair.ExpiresIn,
|
||||
Token = authResult.TokenPair.AccessToken
|
||||
new(ClaimTypes.NameIdentifier, user.Id.ToString()),
|
||||
new(ClaimTypes.Name, user.Username),
|
||||
new(ClaimTypes.Role, "Admin")
|
||||
};
|
||||
|
||||
return Page();
|
||||
var identity = new ClaimsIdentity(claims, AdminAuthDefaults.Scheme);
|
||||
var principal = new ClaimsPrincipal(identity);
|
||||
var properties = new AuthenticationProperties
|
||||
{
|
||||
IsPersistent = RememberMe,
|
||||
RedirectUri = RedirectUrl
|
||||
};
|
||||
|
||||
await HttpContext.SignInAsync(AdminAuthDefaults.Scheme, principal, properties);
|
||||
return LocalRedirect(RedirectUrl);
|
||||
}
|
||||
|
||||
private static string NormalizeRedirectUrl(string? returnUrl)
|
||||
|
||||
@@ -115,7 +115,7 @@ builder.Services.AddSession(options =>
|
||||
builder.Services.AddDistributedMemoryCache();
|
||||
// TempData는 기본적으로 쿠키 저장소 사용 (위 세션 설정 상속)
|
||||
|
||||
// JWT 인증
|
||||
// 인증: 관리자 쿠키 + 포털 쿠키 + 기존 JWT(과도기 API)
|
||||
var connectionString = builder.Configuration.GetConnectionString("Default")
|
||||
?? throw new InvalidOperationException("Missing connection string");
|
||||
var jwtKey = builder.Configuration["Jwt:SecretKey"] ?? throw new InvalidOperationException("Missing JWT SecretKey");
|
||||
@@ -153,6 +153,17 @@ var authenticationBuilder = builder.Services.AddAuthentication(opts =>
|
||||
opts.SlidingExpiration = true;
|
||||
opts.ExpireTimeSpan = TimeSpan.FromDays(7);
|
||||
})
|
||||
.AddCookie(AdminAuthDefaults.Scheme, opts =>
|
||||
{
|
||||
opts.Cookie.Name = AdminAuthDefaults.CookieName;
|
||||
opts.Cookie.HttpOnly = true;
|
||||
opts.Cookie.SameSite = SameSiteMode.Lax;
|
||||
opts.Cookie.SecurePolicy = isProduction ? CookieSecurePolicy.Always : CookieSecurePolicy.SameAsRequest;
|
||||
opts.LoginPath = "/admin/login";
|
||||
opts.AccessDeniedPath = "/admin/login";
|
||||
opts.SlidingExpiration = true;
|
||||
opts.ExpireTimeSpan = TimeSpan.FromHours(12);
|
||||
})
|
||||
.AddCookie(PortalOAuthDefaults.ExternalScheme, opts =>
|
||||
{
|
||||
opts.Cookie.Name = "TaxBaik.Portal.External";
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
namespace TaxBaik.Web.Services;
|
||||
|
||||
public static class AdminAuthDefaults
|
||||
{
|
||||
public const string Scheme = "AdminCookie";
|
||||
public const string CookieName = "TaxBaik.Admin.Auth";
|
||||
}
|
||||
|
||||
@@ -88,6 +88,35 @@ public class AuthService
|
||||
return AuthResult.Success(GenerateTokenPair(user));
|
||||
}
|
||||
|
||||
public async Task<AdminUser?> AuthenticateAsync(string username, string password)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(username) || string.IsNullOrWhiteSpace(password))
|
||||
return null;
|
||||
|
||||
try
|
||||
{
|
||||
var user = await _adminUserRepository.GetByUsernameAsync(username);
|
||||
if (user == null || string.IsNullOrWhiteSpace(user.PasswordHash))
|
||||
return null;
|
||||
|
||||
if (!BCrypt.Net.BCrypt.Verify(password, user.PasswordHash))
|
||||
return null;
|
||||
|
||||
await _adminUserRepository.UpdateLastLoginAtAsync(user.Id);
|
||||
return user;
|
||||
}
|
||||
catch (Exception ex) when (_environment.IsDevelopment())
|
||||
{
|
||||
_logger.LogError(ex, "개발 환경 관리자 인증 실패: {Username}", username);
|
||||
return null;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "관리자 인증 중 오류: {Username}", username);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<AuthTokenPair?> RefreshAccessTokenAsync(string refreshToken)
|
||||
{
|
||||
try
|
||||
|
||||
Reference in New Issue
Block a user