SPMS_API/SPMS.API/Program.cs
SEAN 42aa04f58e improvement: 인증 보안 정책 — Rate Limit + 시도제한 + 보안 로깅 (#190)
- auth_sensitive 명명 Rate Limit 정책 추가 (20회/15분/IP)
- AuthController 3개 + PasswordController 2개 메서드에 EnableRateLimiting 적용
- 로그인 시도 제한 구현 (5회/15분, Redis 카운터, LoginAttemptExceeded 에러코드 활성화)
- 비밀번호 찾기/임시 비밀번호 요청 제한 (3회/1시간, silent 반환)
- AuthService 보안 이벤트 구조적 로깅 (ILogger 주입)
- Swagger 429 응답 문서화

Closes #190
2026-02-25 11:13:49 +09:00

80 lines
2.7 KiB
C#

using System.Threading.RateLimiting;
using Microsoft.AspNetCore.RateLimiting;
using Serilog;
using SPMS.API.Extensions;
using SPMS.Application;
using SPMS.Domain.Common;
using SPMS.Infrastructure;
var builder = WebApplication.CreateBuilder(new WebApplicationOptions
{
WebRootPath = Environment.GetEnvironmentVariable("ASPNETCORE_WEBROOT")
?? "wwwroot"
});
// ===== 1. Serilog =====
builder.Host.UseSerilog((context, config) =>
config.ReadFrom.Configuration(context.Configuration));
// ===== 2. Services (DI) =====
builder.Services.AddApplication();
builder.Services.AddInfrastructure(builder.Configuration);
// ===== 3. Presentation =====
builder.Services.AddControllers()
.ConfigureApiBehaviorOptions(options =>
{
options.InvalidModelStateResponseFactory = context =>
{
var fieldErrors = context.ModelState
.Where(e => e.Value?.Errors.Count > 0)
.SelectMany(e => e.Value!.Errors.Select(err => new FieldError
{
Field = e.Key,
Message = err.ErrorMessage
}))
.ToList();
var response = ApiResponseExtensions.ValidationFail(
ErrorCodes.BadRequest, "입력값 검증 실패", fieldErrors);
return new Microsoft.AspNetCore.Mvc.BadRequestObjectResult(response);
};
});
builder.Services.AddSwaggerDocumentation();
builder.Services.AddJwtAuthentication(builder.Configuration);
builder.Services.AddAuthorizationPolicies();
// ===== 4. Rate Limiting =====
builder.Services.AddRateLimiter(options =>
{
options.RejectionStatusCode = StatusCodes.Status429TooManyRequests;
options.OnRejected = async (context, cancellationToken) =>
{
context.HttpContext.Response.ContentType = "application/json";
var response = ApiResponse.Fail(
ErrorCodes.LimitExceeded, "요청 한도를 초과했습니다. 잠시 후 다시 시도해주세요.");
await context.HttpContext.Response.WriteAsJsonAsync(response, cancellationToken);
};
options.AddFixedWindowLimiter("auth_sensitive", opt =>
{
opt.PermitLimit = 20;
opt.Window = TimeSpan.FromMinutes(15);
});
options.GlobalLimiter = PartitionedRateLimiter.Create<HttpContext, string>(httpContext =>
RateLimitPartition.GetFixedWindowLimiter(
partitionKey: httpContext.Connection.RemoteIpAddress?.ToString() ?? "unknown",
factory: _ => new FixedWindowRateLimiterOptions
{
PermitLimit = 100,
Window = TimeSpan.FromMinutes(1)
}));
});
var app = builder.Build();
// ===== 5. Middleware Pipeline =====
app.UseMiddlewarePipeline();
app.Run();