초기커밋

This commit is contained in:
2025-05-01 07:20:41 +09:00
commit 98bb2e3c5c
2747 changed files with 646947 additions and 0 deletions

View File

@@ -0,0 +1,9 @@
namespace BrokerApiServer.Common;
using Microsoft.AspNetCore.Mvc;
public class ApiControllerBase: ControllerBase
{
protected string PlanetId => HttpContext.Items["planet_id"]?.ToString() ?? string.Empty;
protected string PlanetServerType => HttpContext.Items["planet_server_type"]?.ToString() ?? string.Empty;
}

View File

@@ -0,0 +1,15 @@
using CommandLine;
namespace BrokerApiServer.Common;
public class CommandLineOption
{
[Option('p', "port", Required = true, HelpText = "Server Port")]
public int Port { get; init; } = 12000;
[Option('s', "swagger", Default = false, Required = false, HelpText = "Show Swagger Mode")]
public bool UseSwagger { get; init; } = false;
[Option('n', "named-pipe", Default = false, Required = false, HelpText = "User Named Pipe")]
public bool UseNamedPipe { get; init; } = false;
}

View File

@@ -0,0 +1,6 @@
namespace BrokerApiServer.Common;
public static class Const
{
public static readonly string[] ExcludeLogPaths = ["swagger", "healthcheck"];
}

View File

@@ -0,0 +1,9 @@
namespace BrokerApiServer.Common;
using Microsoft.AspNetCore.Mvc;
public class PlanetAuthControllerBase: ControllerBase
{
protected string PlanetId => HttpContext.Items["planet_id"]?.ToString() ?? string.Empty;
protected string PlanetServerType => HttpContext.Items["planet_server_type"]?.ToString() ?? string.Empty;
}

View File

@@ -0,0 +1,98 @@
using Microsoft.AspNetCore.Mvc.Filters;
namespace BrokerCore.Common;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
using BrokerApiServer.Common;
using Microsoft.IdentityModel.Tokens;
using ServerCore; using ServerBase;
using Services;
/// <summary>
/// 엑세스 토큰 인증이 필요한 컨트롤러를 지정하는 애노테이션(Attribute) 정의
/// 밴 상태 체크 이슈
/// </summary>
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class RequireAdminAuthAttribute : System.Attribute, IAsyncActionFilter
{
public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
{
// require 서비스 가져오기
// var planet_service = context.HttpContext.RequestServices.GetRequiredService<PlanetService>();
// Guard.Against.isNull(planet_service, ServerErrorCode.InternalServerError, "PlanetService가 di에 등록돼 있지 않음");
var auth_header = context.HttpContext.Request.Headers.Authorization.FirstOrDefault() ?? string.Empty;
Guard.Against.isNullOrEmptyOrWhiteSpace(auth_header, ServerErrorCode.InvalidPlanetJwt, ()=>"empty jwt");
Guard.Against.isFalse(auth_header.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase),
ServerErrorCode.InvalidUserJwt, ()=>"인증 토큰 오류");
// "Bearer " 이후의 토큰 부분을 추출합니다.
var token = auth_header["Bearer ".Length..].Trim();
context.HttpContext.Items["admin_id"] = validate(token);
await next();
}
//==========================================
// secret_key: '81b659967735aea6e4cb0467d04ea12c4a6432b415254f59825055680f59a9823fec5a15e9adbd246b1365ef1522580477691bc5cb56a9364143e7d9385d9912'
// jdbc-url: jdbc:mysql://10.20.20.8:3306/caliverse?useUnicode=true&characterEncoding=UTF-8&serverTimezone=GMT%2B8
// username: external_ro
// password: bQNEXbRWQTtV6bwlqktGyBiuf2KqYF
// token
// eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJiZW9tY2h1bC5qYW5nQGxvdHRlLm5ldCIsImlhdCI6MTcyOTc1MzkzNiwiZXhwIjoxNzI5ODQwMzM2fQ.TzLFPigDZIYkxZa3JNdE2kHPxTBkCevwqtcWgz8tMnA
//==========================================
// todo: admin 관련 시크릿을 config에서 정의하여 사용할 것
private string validate(string jwt)
{
const string admin_pass_token = "p8qcZBraFCGfm2QeIGkJBynb6ULKhi6wGlnCDXvKTnM";
if (jwt == admin_pass_token)
{
return "park.chanheon@lotte.net";
}
var principal = parseToken(jwt);
Guard.Against.isNull(principal, ServerErrorCode.InvalidUserJwt, ()=>"jwt parsing error");
// var exp_time = DateTimeOffset.FromUnixTimeSeconds(long.Parse(exp_claim ?? string.Empty));
// Guard.Against.isFalse(exp_time > DateTimeOffset.UtcNow, ServerErrorCode.ExpiredPlanetJwt, "Jwt has expired");
return principal.FindFirstValue(JwtRegisteredClaimNames.Sub) ?? string.Empty;
}
public ClaimsPrincipal? parseToken(string token)
{
const string admin_secret_key = "81b659967735aea6e4cb0467d04ea12c4a6432b415254f59825055680f59a9823fec5a15e9adbd246b1365ef1522580477691bc5cb56a9364143e7d9385d9912";
var token_handler = new JwtSecurityTokenHandler();
// 시크릿 키를 바이트 배열로 변환
var key = Encoding.UTF8.GetBytes(admin_secret_key);
// 토큰 검증 매개변수 설정
var validation_parameters = new TokenValidationParameters
{
ValidateIssuer = false,
ValidateAudience = false,
ValidIssuer = "",
ValidAudience = "",
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(key)
};
try
{
// 토큰 검증 및 클레임 추출
var principal = token_handler.ValidateToken(token, validation_parameters, out var validated_token);
return principal;
}
catch (SecurityTokenException ex)
{
Log.getLogger().error($"admin JWT 파싱 에러 => {ex.Message}");
}
return null;
}
}

View File

@@ -0,0 +1,34 @@
using Microsoft.AspNetCore.Mvc.Filters;
namespace BrokerCore.Common;
using BrokerApiServer.Common;
using Services;
/// <summary>
/// 엑세스 토큰 인증이 필요한 컨트롤러를 지정하는 애노테이션(Attribute) 정의
/// 밴 상태 체크 이슈
/// </summary>
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class RequirePlanetAuthAttribute : System.Attribute, IAsyncActionFilter
{
public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
{
// require 서비스 가져오기
var planet_service = context.HttpContext.RequestServices.GetRequiredService<PlanetService>();
Guard.Against.isNull(planet_service, ServerErrorCode.InternalServerError, ()=>"PlanetService가 di에 등록돼 있지 않음");
var auth_header = context.HttpContext.Request.Headers.Authorization.FirstOrDefault() ?? string.Empty;
Guard.Against.isNullOrEmptyOrWhiteSpace(auth_header, ServerErrorCode.InvalidPlanetJwt, ()=>"empty jwt");
Guard.Against.isFalse(auth_header.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase),
ServerErrorCode.InvalidUserJwt, ()=>"인증 토큰 오류");
// "Bearer " 이후의 토큰 부분을 추출합니다.
var token = auth_header["Bearer ".Length..].Trim();
var (planet_id, planet_server_type) = planet_service.validate(token);
context.HttpContext.Items["planet_id"] = planet_id;
context.HttpContext.Items["planet_server_type"] = planet_server_type;
await next();
}
}

View File

@@ -0,0 +1,27 @@
namespace BrokerCore.Common;
using Microsoft.AspNetCore.Mvc.Filters;
using Services;
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class RequireUserJwtAuthAttribute : System.Attribute, IAsyncActionFilter
{
public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
{
var user_auth_service = context.HttpContext.RequestServices.GetRequiredService<UserAuthService>();
Guard.Against.isNull(user_auth_service, ServerErrorCode.InternalServerError, ()=>"PlanetService가 di에 등록돼 있지 않음");
var auth_header = context.HttpContext.Request.Headers.Authorization.FirstOrDefault() ?? string.Empty;
Guard.Against.isNullOrEmptyOrWhiteSpace(auth_header, ServerErrorCode.InvalidUserJwt, ()=>"empty jwt");
Guard.Against.isFalse(auth_header.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase),
ServerErrorCode.InvalidUserJwt, ()=>"인증 토큰 오류");
// "Bearer " 이후의 토큰 부분을 추출합니다.
var token = auth_header["Bearer ".Length..].Trim();
var result = await user_auth_service.authByWebPortalToken(token);
Guard.Against.resultFail(result);
context.HttpContext.Items["user_guid"] = user_auth_service.UserGuid;
await next();
}
}

View File

@@ -0,0 +1,36 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using BrokerApiServer.Common;
using BrokerCore.ApiModels;
using BrokerCore.Common;
namespace CaliGameApi.Middlewares;
using ServerCore;
using ServerBase;
public class ResultExceptionFilter : IExceptionFilter
{
public void OnException(ExceptionContext context)
{
var error_code = (int)ServerErrorCode.InternalServerError;
if (context.Exception is ApiException exception)
{
error_code = exception.ErrorCode;
}
var data = new ApiErrorResponse
{
TraceId = context.HttpContext.TraceIdentifier,
ErrorCode = error_code,
ErrorMessage =
$"{context.Exception.Message} path:{context.HttpContext.Request.Path}",
};
Log.getLogger().error($"Response trace_id {context.HttpContext.TraceIdentifier} {context.Exception.StackTrace}");
// 다른 예외를 호출하지 않도록 설정
context.ExceptionHandled = true;
context.Result = new ObjectResult(data) { StatusCode = StatusCodes.Status400BadRequest };
}
}

View File

@@ -0,0 +1,67 @@
namespace BrokerApiServer.Common;
using ServerCore; using ServerBase;
public class ResultLoggingMiddleware : IMiddleware
{
public async Task InvokeAsync(HttpContext context, RequestDelegate next)
{
var path = context.Request.Path.Value;
if (Const.ExcludeLogPaths.Any(excludeLogPath => path?.Contains(excludeLogPath) ?? false))
{
await next(context);
return;
}
// 요청 로깅
var trace_id = context.TraceIdentifier;
var request_body = await readRequestBody(context.Request);
Log.getLogger()
.Info($"Request trace_id {trace_id} method {context.Request.Method} path {path}, req {request_body}");
// 응답 로깅을 위해 응답 본문을 캡처
var original_body_stream = context.Response.Body;
using var response_body_stream = new MemoryStream();
context.Response.Body = response_body_stream;
// 다음 미들웨어로 요청 전달
await next(context);
// 200 OK 응답 로깅 - 에러인 경우 에러 필터에서 처리
var response_body = await readResponseBody(context.Response);
var response =
$"trace_id {trace_id} status {context.Response.StatusCode}, path {path}, res {response_body}, req {request_body} ";
if (context.Response.StatusCode == StatusCodes.Status200OK)
{
// 불필요한 로그를 남기지 않은 healthcheck, swagger 제외
if (Const.ExcludeLogPaths.Any(excludeLogPath => path?.Contains(excludeLogPath) ?? false))
{
return;
}
Log.getLogger().info($"Response {response}");
}
else
{
Log.getLogger().error($"Response {response}");
}
// 응답 본문을 원래 스트림으로 복사
await response_body_stream.CopyToAsync(original_body_stream);
}
private async Task<string> readRequestBody(HttpRequest request)
{
request.EnableBuffering();
var body = await new StreamReader(request.Body).ReadToEndAsync();
request.Body.Position = 0;
return body;
}
private async Task<string> readResponseBody(HttpResponse response)
{
response.Body.Seek(0, SeekOrigin.Begin);
var body = await new StreamReader(response.Body).ReadToEndAsync();
response.Body.Seek(0, SeekOrigin.Begin);
return body;
}
}

View File

@@ -0,0 +1,140 @@
using System.Reflection;
using Microsoft.OpenApi.Models;
using Swashbuckle.AspNetCore.SwaggerGen;
namespace BrokerApiServer.Common;
using Microsoft.OpenApi.Any;
// 사용자 정의 스키마 필터 예시: Enum을 문자열로 표현
public class EnumAsStringSchemaFilter : ISchemaFilter
{
public void Apply(OpenApiSchema schema, SchemaFilterContext context)
{
if (context.Type.IsEnum)
{
schema.Type = "string";
schema.Enum = Enum.GetNames(context.Type)
.Select(name => new OpenApiString(name))
.Cast<IOpenApiAny>()
.ToList();
}
}
}
[AttributeUsage(AttributeTargets.Method)]
public class SwaggerRequestBodyWithSchemaAttribute : Attribute
{
public string Description { get; }
public Type Type { get; }
public SwaggerRequestBodyWithSchemaAttribute(string description, Type type)
{
Description = description;
Type = type;
}
}
public class SwaggerRequestBodyWithSchemaFilter : IOperationFilter
{
public void Apply(OpenApiOperation operation, OperationFilterContext context)
{
var methodInfo = context.MethodInfo;
// 커스텀 특성 가져오기
if (methodInfo.GetCustomAttributes(typeof(SwaggerRequestBodyWithSchemaAttribute), false)
.FirstOrDefault() is SwaggerRequestBodyWithSchemaAttribute attribute)
{
// 스키마 생성
var schema = context.SchemaGenerator.GenerateSchema(attribute.Type, context.SchemaRepository);
// 스키마 정보를 문자열로 변환
var schema_description = getSchemaDescription(schema);
// 기존 Description과 병합
var description = $"{attribute.Description}\n\n**스키마 정보:**\n{schema_description}";
if (operation.RequestBody != null)
{
operation.RequestBody.Description = description;
}
else
{
// RequestBody가 없을 경우 새로 생성
operation.RequestBody = new OpenApiRequestBody
{
Description = description,
Required = true,
Content = new Dictionary<string, OpenApiMediaType>
{
["application/json"] = new OpenApiMediaType
{
Schema = schema
}
}
};
}
}
}
private string getSchemaDescription(OpenApiSchema schema)
{
if (schema.Properties != null && schema.Properties.Any())
{
return string.Join("\n", schema.Properties.Select(prop =>
{
var prop_schema = prop.Value;
var type = prop_schema.Type ?? "object";
var format = !string.IsNullOrEmpty(prop_schema.Format) ? $" ({prop_schema.Format})" : "";
var description = !string.IsNullOrEmpty(prop_schema.Description) ? $": {prop_schema.Description}" : "";
return $"- **{prop.Key}** ({type}{format}){description}";
}));
}
else
{
return "스키마에 정의된 프로퍼티가 없습니다.";
}
}
}
public class SwaggerSettingHelper
{
public static void setSwaggerGen(SwaggerGenOptions genOptions)
{
genOptions.SwaggerDoc("v1", new OpenApiInfo { Title = "Metaverse Broker Api", Version = "v1" });
var xml_file = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
var xml_path = Path.Combine(AppContext.BaseDirectory, xml_file);
genOptions.IncludeXmlComments(xml_path);
genOptions.EnableAnnotations();
genOptions.SchemaFilter<EnumAsStringSchemaFilter>();
genOptions.OperationFilter<SwaggerRequestBodyWithSchemaFilter>();
genOptions.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
{
Name = "Authorization",
Type = SecuritySchemeType.Http,
Scheme = "Bearer",
BearerFormat = "JWT",
In = ParameterLocation.Header,
Description = "JWT Authorization header using the Bearer scheme."
});
genOptions.AddSecurityRequirement(new OpenApiSecurityRequirement
{
{
new OpenApiSecurityScheme
{
Reference = new OpenApiReference
{
Type = ReferenceType.SecurityScheme,
Id = "Bearer"
}
},
new string [] {}
}
});
}
}