fix: 로그인 HTTP 자기호출 완전 제거 — IWorkspaceRepository 직접 DI 주입
WBS-9.3 - NULL Policy CI Gate / NULL Policy Validation (push) Failing after 9s
Quant Engine CI/CD Pipeline / validate-core (push) Failing after 18s
Quant Engine CI/CD Pipeline / validate-ui-and-storage (push) Has been skipped
Deploy to Production / Build & Deploy to Production (push) Successful in 2m30s

이전 수정(34df08d)에서 localhost:5265로 고정했으나,
프로덕션 서버는 포트 5000으로 실행 중이어서 Connection refused 발생.

근본 원인: Razor 로그인 페이지가 자기 자신의 API를 HTTP로 재호출하는 구조.

해결:
- HttpClient 자기호출 완전 제거
- IWorkspaceRepository를 Razor 페이지에 직접 DI 주입
- DB 조회 → SHA-256 해시 검증 → 세션 발급 → 쿠키 설정을 인라인 처리
- 포트/프록시 의존성 완전 제거
This commit is contained in:
2026-07-06 17:38:46 +09:00
parent 8d72216959
commit d5ede69800
@@ -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<LoginModel> _logger;
public string? Username { get; set; }
public bool RememberUsername { get; set; }
public string? ErrorMessage { get; set; }
public LoginModel(HttpClient httpClient, ILogger<LoginModel> logger)
public LoginModel(IWorkspaceRepository workspaceRepo, ILogger<LoginModel> logger)
{
_httpClient = httpClient;
_workspaceRepo = workspaceRepo;
_logger = logger;
}
@@ -41,24 +45,69 @@ 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);
// Direct repository call — no internal HTTP round-trip.
// Using IWorkspaceRepository injected via DI avoids any port/proxy dependency.
WorkspaceAccount? account = null;
try
{
account = await _workspaceRepo.GetAccountByUsernameAsync(username.Trim());
}
catch (Exception dbEx)
{
_logger.LogError(dbEx, "[Login] Database lookup failed for user '{Username}'", username);
ErrorMessage = "데이터베이스 연결 오류가 발생했습니다. 잠시 후 다시 시도해 주세요.";
Username = username;
RememberUsername = rememberUsername;
return Page();
}
if (response.IsSuccessStatusCode)
if (account is null || !string.Equals(account.IsActive, "true", StringComparison.OrdinalIgnoreCase))
{
// 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);
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(
@@ -77,16 +126,9 @@ namespace QuantEngine.Web.Pages.Account
Response.Cookies.Delete("quant_admin_username");
}
_logger.LogInformation("[Login] User '{Username}' authenticated successfully", account.Username);
return Redirect("/");
}
else
{
ErrorMessage = "로그인 실패: 아이디 또는 비밀번호가 올바르지 않습니다.";
Username = username;
RememberUsername = rememberUsername;
return Page();
}
}
catch (Exception ex)
{
_logger.LogError(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");
}