초기커밋
This commit is contained in:
9
BrokerApiServer/Common/ApiControllerBase.cs
Normal file
9
BrokerApiServer/Common/ApiControllerBase.cs
Normal 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;
|
||||
}
|
||||
15
BrokerApiServer/Common/CommandLineOption.cs
Normal file
15
BrokerApiServer/Common/CommandLineOption.cs
Normal 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;
|
||||
}
|
||||
6
BrokerApiServer/Common/Const.cs
Normal file
6
BrokerApiServer/Common/Const.cs
Normal file
@@ -0,0 +1,6 @@
|
||||
namespace BrokerApiServer.Common;
|
||||
|
||||
public static class Const
|
||||
{
|
||||
public static readonly string[] ExcludeLogPaths = ["swagger", "healthcheck"];
|
||||
}
|
||||
9
BrokerApiServer/Common/PlanetAuthControllerBase.cs
Normal file
9
BrokerApiServer/Common/PlanetAuthControllerBase.cs
Normal 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;
|
||||
}
|
||||
98
BrokerApiServer/Common/RequireAdminAuthAttribute.cs
Normal file
98
BrokerApiServer/Common/RequireAdminAuthAttribute.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
34
BrokerApiServer/Common/RequirePlanetAuthAttribute.cs
Normal file
34
BrokerApiServer/Common/RequirePlanetAuthAttribute.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
27
BrokerApiServer/Common/RequireUserJwtAuthAttribute.cs
Normal file
27
BrokerApiServer/Common/RequireUserJwtAuthAttribute.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
36
BrokerApiServer/Common/ResultExceptionFilter.cs
Normal file
36
BrokerApiServer/Common/ResultExceptionFilter.cs
Normal 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 };
|
||||
}
|
||||
}
|
||||
67
BrokerApiServer/Common/ResultLoggingMiddleware.cs
Normal file
67
BrokerApiServer/Common/ResultLoggingMiddleware.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
140
BrokerApiServer/Common/SwaggerSettingHelper.cs
Normal file
140
BrokerApiServer/Common/SwaggerSettingHelper.cs
Normal 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 [] {}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user