using Microsoft.AspNetCore.Http; using System.Security.Claims; namespace SharedKernel.Extensions; public static class HttpContextExtension { // 회원 ID 조회 public static int GetMemberID(this HttpContext context) { return int.TryParse(context.User.FindFirst(ClaimTypes.NameIdentifier)?.Value, out var id) ? id : 0; } // 회원 이름 조회 public static string? UserName(this HttpContext context) { return context.User.Identity?.Name; } // 이전 요청 URL 조회 public static string? GetReferer(this HttpContext context) { if (context.Request.Headers.TryGetValue("Referer", out var referer) && !string.IsNullOrWhiteSpace(referer)) { return referer.ToString(); } return null; } // Client IP 조회 — Cloudflare → IIS → backend reverse proxy 환경 대응 // 우선순위: // 0) X-Original-Client-IP (Next.js proxy 가 Cloudflare 더블 경유 우회용으로 forward 한 진짜 user IP) // 1) CF-Connecting-IP (Cloudflare 가 보장하는 client IP, 단 더블 경유 시 Next.js server IP) // 2) X-Real-IP (일부 proxy 가 채움) // 3) X-Forwarded-For (comma-separated 의 가장 왼쪽 = 원래 client, 첫 값만) // 4) RemoteIpAddress (ForwardedHeaders middleware 가 채워주는 경우) // IPv4-mapped IPv6 (`::ffff:127.0.0.1`) → `127.0.0.1` 로 정규화. IP 가 아닌 값은 45 char 로 방어 제한. public static string? GetClientIP(this HttpContext context) { if (context.Request.Headers.TryGetValue("X-Original-Client-IP", out var orig) && !string.IsNullOrWhiteSpace(orig)) { return NormalizeIP(orig.ToString().Trim()); } if (context.Request.Headers.TryGetValue("CF-Connecting-IP", out var cf) && !string.IsNullOrWhiteSpace(cf)) { return NormalizeIP(cf.ToString().Trim()); } if (context.Request.Headers.TryGetValue("X-Real-IP", out var xri) && !string.IsNullOrWhiteSpace(xri)) { return NormalizeIP(xri.ToString().Trim()); } var forwardedFor = context.Request.Headers["X-Forwarded-For"].FirstOrDefault(); if (!string.IsNullOrWhiteSpace(forwardedFor)) { // comma-separated 의 가장 왼쪽 = 원래 client (RFC 7239 / standard) var first = forwardedFor.Split(',', 2, StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries).FirstOrDefault(); if (!string.IsNullOrWhiteSpace(first)) { return NormalizeIP(first); } } return NormalizeIP(context.Connection.RemoteIpAddress?.ToString()); } private static string? NormalizeIP(string? ip) { if (string.IsNullOrWhiteSpace(ip)) { return null; } // IPv4-mapped IPv6 (e.g. "::ffff:127.0.0.1") → IPv4 만 추출 if (System.Net.IPAddress.TryParse(ip, out var parsed)) { if (parsed.IsIPv4MappedToIPv6) { return parsed.MapToIPv4().ToString(); } return parsed.ToString(); } // IP 형식이 아니면 컬럼 최대 길이(45)로 방어적 제한 return ip.Length > 45 ? ip[..45] : ip; } // User-agent 조회 — Next.js Route Handler 가 forward 한 X-Original-User-Agent 최우선. public static string? GetUserAgent(this HttpContext context) { if (context.Request.Headers.TryGetValue("X-Original-User-Agent", out var origUa) && !string.IsNullOrWhiteSpace(origUa)) { return origUa.ToString(); } if (context.Request.Headers.TryGetValue("User-Agent", out var userAgent) && !string.IsNullOrWhiteSpace(userAgent)) { return userAgent.ToString(); } return null; } // 상관관계 ID 조회 public static string GetCorrelationID(this HttpContext context) { return context.TraceIdentifier; } }