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.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||||
|
using QuantEngine.Core.Interfaces;
|
||||||
|
using QuantEngine.Core.Models;
|
||||||
|
|
||||||
namespace QuantEngine.Web.Pages.Account
|
namespace QuantEngine.Web.Pages.Account
|
||||||
{
|
{
|
||||||
[AllowAnonymous]
|
[AllowAnonymous]
|
||||||
public class LoginModel : PageModel
|
public class LoginModel : PageModel
|
||||||
{
|
{
|
||||||
private readonly HttpClient _httpClient;
|
private readonly IWorkspaceRepository _workspaceRepo;
|
||||||
private readonly ILogger<LoginModel> _logger;
|
private readonly ILogger<LoginModel> _logger;
|
||||||
|
|
||||||
public string? Username { get; set; }
|
public string? Username { get; set; }
|
||||||
public bool RememberUsername { get; set; }
|
public bool RememberUsername { get; set; }
|
||||||
public string? ErrorMessage { 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;
|
_logger = logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -41,51 +45,89 @@ namespace QuantEngine.Web.Pages.Account
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var loginRequest = new { Username = username, Password = password };
|
// Direct repository call — no internal HTTP round-trip.
|
||||||
// Always call the internal API directly to avoid Cloudflare proxy interference.
|
// Using IWorkspaceRepository injected via DI avoids any port/proxy dependency.
|
||||||
// Request.Host / Request.Scheme must NOT be used here — they resolve to the external
|
WorkspaceAccount? account = null;
|
||||||
// domain which causes the self-call to go out through Cloudflare and return 400.
|
try
|
||||||
var requestUri = "http://localhost:5265/api/auth/login";
|
|
||||||
var response = await _httpClient.PostAsJsonAsync(requestUri, loginRequest);
|
|
||||||
|
|
||||||
if (response.IsSuccessStatusCode)
|
|
||||||
{
|
{
|
||||||
// Extract set-cookie header from API response and set it on proxy client response
|
account = await _workspaceRepo.GetAccountByUsernameAsync(username.Trim());
|
||||||
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("/");
|
|
||||||
}
|
}
|
||||||
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 = "로그인 실패: 아이디 또는 비밀번호가 올바르지 않습니다.";
|
ErrorMessage = "로그인 실패: 아이디 또는 비밀번호가 올바르지 않습니다.";
|
||||||
Username = username;
|
Username = username;
|
||||||
RememberUsername = rememberUsername;
|
RememberUsername = rememberUsername;
|
||||||
return Page();
|
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)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -99,7 +141,6 @@ namespace QuantEngine.Web.Pages.Account
|
|||||||
|
|
||||||
public IActionResult OnGetLogout()
|
public IActionResult OnGetLogout()
|
||||||
{
|
{
|
||||||
// Revoke cookies securely
|
|
||||||
Response.Cookies.Delete("quant_auth_token");
|
Response.Cookies.Delete("quant_auth_token");
|
||||||
return Redirect("/Account/Login");
|
return Redirect("/Account/Login");
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user