From d5ede6980067800c92424cd9381774510caaf647 Mon Sep 17 00:00:00 2001 From: kjh2064 Date: Mon, 6 Jul 2026 17:38:46 +0900 Subject: [PATCH] =?UTF-8?q?fix:=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20HTTP=20?= =?UTF-8?q?=EC=9E=90=EA=B8=B0=ED=98=B8=EC=B6=9C=20=EC=99=84=EC=A0=84=20?= =?UTF-8?q?=EC=A0=9C=EA=B1=B0=20=E2=80=94=20IWorkspaceRepository=20?= =?UTF-8?q?=EC=A7=81=EC=A0=91=20DI=20=EC=A3=BC=EC=9E=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 이전 수정(34df08d)에서 localhost:5265로 고정했으나, 프로덕션 서버는 포트 5000으로 실행 중이어서 Connection refused 발생. 근본 원인: Razor 로그인 페이지가 자기 자신의 API를 HTTP로 재호출하는 구조. 해결: - HttpClient 자기호출 완전 제거 - IWorkspaceRepository를 Razor 페이지에 직접 DI 주입 - DB 조회 → SHA-256 해시 검증 → 세션 발급 → 쿠키 설정을 인라인 처리 - 포트/프록시 의존성 완전 제거 --- .../Pages/Account/Login.cshtml.cs | 123 ++++++++++++------ 1 file changed, 82 insertions(+), 41 deletions(-) diff --git a/src/dotnet/QuantEngine.Web/Pages/Account/Login.cshtml.cs b/src/dotnet/QuantEngine.Web/Pages/Account/Login.cshtml.cs index 03295a9..86f9267 100644 --- a/src/dotnet/QuantEngine.Web/Pages/Account/Login.cshtml.cs +++ b/src/dotnet/QuantEngine.Web/Pages/Account/Login.cshtml.cs @@ -1,22 +1,26 @@ +using System.Security.Cryptography; +using System.Text; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.RazorPages; +using QuantEngine.Core.Interfaces; +using QuantEngine.Core.Models; namespace QuantEngine.Web.Pages.Account { [AllowAnonymous] public class LoginModel : PageModel { - private readonly HttpClient _httpClient; + private readonly IWorkspaceRepository _workspaceRepo; private readonly ILogger _logger; public string? Username { get; set; } public bool RememberUsername { get; set; } public string? ErrorMessage { get; set; } - public LoginModel(HttpClient httpClient, ILogger logger) + public LoginModel(IWorkspaceRepository workspaceRepo, ILogger logger) { - _httpClient = httpClient; + _workspaceRepo = workspaceRepo; _logger = logger; } @@ -41,51 +45,89 @@ namespace QuantEngine.Web.Pages.Account try { - var loginRequest = new { Username = username, Password = password }; - // Always call the internal API directly to avoid Cloudflare proxy interference. - // Request.Host / Request.Scheme must NOT be used here — they resolve to the external - // domain which causes the self-call to go out through Cloudflare and return 400. - var requestUri = "http://localhost:5265/api/auth/login"; - var response = await _httpClient.PostAsJsonAsync(requestUri, loginRequest); - - if (response.IsSuccessStatusCode) + // Direct repository call — no internal HTTP round-trip. + // Using IWorkspaceRepository injected via DI avoids any port/proxy dependency. + WorkspaceAccount? account = null; + try { - // Extract set-cookie header from API response and set it on proxy client response - if (response.Headers.TryGetValues("Set-Cookie", out var cookieHeaders)) - { - foreach (var cookieHeader in cookieHeaders) - { - Response.Headers.Append("Set-Cookie", cookieHeader); - } - } - - if (rememberUsername) - { - Response.Cookies.Append( - "quant_admin_username", - username, - new Microsoft.AspNetCore.Http.CookieOptions - { - Expires = DateTimeOffset.UtcNow.AddDays(30), - HttpOnly = false, - SameSite = Microsoft.AspNetCore.Http.SameSiteMode.Strict - } - ); - } - else - { - Response.Cookies.Delete("quant_admin_username"); - } - - return Redirect("/"); + account = await _workspaceRepo.GetAccountByUsernameAsync(username.Trim()); } - else + catch (Exception dbEx) + { + _logger.LogError(dbEx, "[Login] Database lookup failed for user '{Username}'", username); + ErrorMessage = "데이터베이스 연결 오류가 발생했습니다. 잠시 후 다시 시도해 주세요."; + Username = username; + RememberUsername = rememberUsername; + return Page(); + } + + if (account is null || !string.Equals(account.IsActive, "true", StringComparison.OrdinalIgnoreCase)) { ErrorMessage = "로그인 실패: 아이디 또는 비밀번호가 올바르지 않습니다."; Username = username; RememberUsername = rememberUsername; return Page(); } + + var passwordHash = Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(password))); + if (!string.Equals(account.PasswordHash, passwordHash, StringComparison.OrdinalIgnoreCase)) + { + ErrorMessage = "로그인 실패: 아이디 또는 비밀번호가 올바르지 않습니다."; + Username = username; + RememberUsername = rememberUsername; + return Page(); + } + + // Issue session token + var rawToken = Guid.NewGuid().ToString("N"); + var tokenHash = Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(rawToken))); + var now = DateTimeOffset.UtcNow; + var expiresAt = now.AddDays(7); + + await _workspaceRepo.UpsertSessionAsync(new WorkspaceSession + { + SessionTokenHash = tokenHash, + Username = account.Username, + Role = account.Role, + CreatedAt = now.ToString("O"), + ExpiresAt = expiresAt.ToString("O"), + RevokedAt = null + }); + + // Set HTTP-only auth cookie (Secure=true since production is always HTTPS via Cloudflare) + Response.Cookies.Append( + "quant_auth_token", + rawToken, + new Microsoft.AspNetCore.Http.CookieOptions + { + HttpOnly = true, + Secure = true, + SameSite = Microsoft.AspNetCore.Http.SameSiteMode.Lax, + Expires = expiresAt, + Path = "/" + } + ); + + if (rememberUsername) + { + Response.Cookies.Append( + "quant_admin_username", + username, + new Microsoft.AspNetCore.Http.CookieOptions + { + Expires = DateTimeOffset.UtcNow.AddDays(30), + HttpOnly = false, + SameSite = Microsoft.AspNetCore.Http.SameSiteMode.Strict + } + ); + } + else + { + Response.Cookies.Delete("quant_admin_username"); + } + + _logger.LogInformation("[Login] User '{Username}' authenticated successfully", account.Username); + return Redirect("/"); } catch (Exception ex) { @@ -99,7 +141,6 @@ namespace QuantEngine.Web.Pages.Account public IActionResult OnGetLogout() { - // Revoke cookies securely Response.Cookies.Delete("quant_auth_token"); return Redirect("/Account/Login"); }