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
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:
@@ -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,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");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user