feat: X-Service-Code / X-API-KEY 서비스 식별 미들웨어 구현 (#32)
- ServiceRepository: IServiceRepository 구현 (GetByServiceCode, GetByApiKey) - ServiceCodeMiddleware: X-Service-Code 헤더 검증, DB 조회, 서비스 상태 확인 - ApiKeyMiddleware: /v1/in/device/* 경로 X-API-KEY 검증 - ApplicationBuilderExtensions: 미들웨어 파이프라인 12~13번 등록 - DependencyInjection: IServiceRepository DI 등록 Closes #32
This commit is contained in:
parent
4270e70f09
commit
df8a8e2e5b
|
|
@ -56,11 +56,11 @@ public static class ApplicationBuilderExtensions
|
||||||
// -- 11. 역할 인가 --
|
// -- 11. 역할 인가 --
|
||||||
app.UseAuthorization();
|
app.UseAuthorization();
|
||||||
|
|
||||||
// -- 12. X-Service-Code 서비스 식별 (미구현 — Issue #13) --
|
// -- 12. X-Service-Code 서비스 식별 --
|
||||||
// app.UseMiddleware<ServiceCodeMiddleware>();
|
app.UseMiddleware<ServiceCodeMiddleware>();
|
||||||
|
|
||||||
// -- 13. X-API-KEY 검증 (미구현 — Issue #13) --
|
// -- 13. X-API-KEY 검증 (SDK/디바이스 엔드포인트용) --
|
||||||
// app.UseMiddleware<ApiKeyMiddleware>();
|
app.UseMiddleware<ApiKeyMiddleware>();
|
||||||
|
|
||||||
// -- 14. X-SPMS-TEST 샌드박스 모드 (미구현 — Issue #14) --
|
// -- 14. X-SPMS-TEST 샌드박스 모드 (미구현 — Issue #14) --
|
||||||
// app.UseMiddleware<SandboxMiddleware>();
|
// app.UseMiddleware<SandboxMiddleware>();
|
||||||
|
|
|
||||||
49
SPMS.API/Middlewares/ApiKeyMiddleware.cs
Normal file
49
SPMS.API/Middlewares/ApiKeyMiddleware.cs
Normal file
|
|
@ -0,0 +1,49 @@
|
||||||
|
using SPMS.Domain.Common;
|
||||||
|
using SPMS.Domain.Interfaces;
|
||||||
|
|
||||||
|
namespace SPMS.API.Middlewares;
|
||||||
|
|
||||||
|
public class ApiKeyMiddleware
|
||||||
|
{
|
||||||
|
private readonly RequestDelegate _next;
|
||||||
|
|
||||||
|
public ApiKeyMiddleware(RequestDelegate next) => _next = next;
|
||||||
|
|
||||||
|
public async Task InvokeAsync(HttpContext context, IServiceRepository serviceRepository)
|
||||||
|
{
|
||||||
|
if (!RequiresApiKey(context.Request.Path))
|
||||||
|
{
|
||||||
|
await _next(context);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!context.Request.Headers.TryGetValue("X-API-KEY", out var apiKey) ||
|
||||||
|
string.IsNullOrWhiteSpace(apiKey))
|
||||||
|
{
|
||||||
|
context.Response.StatusCode = 401;
|
||||||
|
context.Response.ContentType = "application/json";
|
||||||
|
await context.Response.WriteAsJsonAsync(
|
||||||
|
ApiResponse.Fail(ErrorCodes.Unauthorized, "API Key가 필요합니다."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var service = await serviceRepository.GetByApiKeyAsync(apiKey!);
|
||||||
|
if (service == null)
|
||||||
|
{
|
||||||
|
context.Response.StatusCode = 403;
|
||||||
|
context.Response.ContentType = "application/json";
|
||||||
|
await context.Response.WriteAsJsonAsync(
|
||||||
|
ApiResponse.Fail(ErrorCodes.Unauthorized, "유효하지 않은 API Key입니다."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
context.Items["Service"] = service;
|
||||||
|
context.Items["ServiceId"] = service.Id;
|
||||||
|
await _next(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool RequiresApiKey(PathString path)
|
||||||
|
{
|
||||||
|
return path.StartsWithSegments("/v1/in/device");
|
||||||
|
}
|
||||||
|
}
|
||||||
56
SPMS.API/Middlewares/ServiceCodeMiddleware.cs
Normal file
56
SPMS.API/Middlewares/ServiceCodeMiddleware.cs
Normal file
|
|
@ -0,0 +1,56 @@
|
||||||
|
using SPMS.Domain.Common;
|
||||||
|
using SPMS.Domain.Enums;
|
||||||
|
using SPMS.Domain.Interfaces;
|
||||||
|
|
||||||
|
namespace SPMS.API.Middlewares;
|
||||||
|
|
||||||
|
public class ServiceCodeMiddleware
|
||||||
|
{
|
||||||
|
private readonly RequestDelegate _next;
|
||||||
|
|
||||||
|
public ServiceCodeMiddleware(RequestDelegate next) => _next = next;
|
||||||
|
|
||||||
|
public async Task InvokeAsync(HttpContext context, IServiceRepository serviceRepository)
|
||||||
|
{
|
||||||
|
if (context.Request.Path.StartsWithSegments("/v1/out") ||
|
||||||
|
context.Request.Path.StartsWithSegments("/swagger") ||
|
||||||
|
context.Request.Path.StartsWithSegments("/health"))
|
||||||
|
{
|
||||||
|
await _next(context);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!context.Request.Headers.TryGetValue("X-Service-Code", out var serviceCode) ||
|
||||||
|
string.IsNullOrWhiteSpace(serviceCode))
|
||||||
|
{
|
||||||
|
context.Response.StatusCode = 400;
|
||||||
|
context.Response.ContentType = "application/json";
|
||||||
|
await context.Response.WriteAsJsonAsync(
|
||||||
|
ApiResponse.Fail(ErrorCodes.BadRequest, "X-Service-Code 헤더가 필요합니다."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var service = await serviceRepository.GetByServiceCodeAsync(serviceCode!);
|
||||||
|
if (service == null)
|
||||||
|
{
|
||||||
|
context.Response.StatusCode = 404;
|
||||||
|
context.Response.ContentType = "application/json";
|
||||||
|
await context.Response.WriteAsJsonAsync(
|
||||||
|
ApiResponse.Fail(ErrorCodes.NotFound, "존재하지 않는 서비스입니다."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (service.Status != ServiceStatus.Active)
|
||||||
|
{
|
||||||
|
context.Response.StatusCode = 503;
|
||||||
|
context.Response.ContentType = "application/json";
|
||||||
|
await context.Response.WriteAsJsonAsync(
|
||||||
|
ApiResponse.Fail(ErrorCodes.Unauthorized, "비활성 상태의 서비스입니다."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
context.Items["Service"] = service;
|
||||||
|
context.Items["ServiceId"] = service.Id;
|
||||||
|
await _next(context);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -24,6 +24,7 @@ public static class DependencyInjection
|
||||||
// UnitOfWork & Repositories
|
// UnitOfWork & Repositories
|
||||||
services.AddScoped<IUnitOfWork, UnitOfWork>();
|
services.AddScoped<IUnitOfWork, UnitOfWork>();
|
||||||
services.AddScoped(typeof(IRepository<>), typeof(Repository<>));
|
services.AddScoped(typeof(IRepository<>), typeof(Repository<>));
|
||||||
|
services.AddScoped<IServiceRepository, ServiceRepository>();
|
||||||
|
|
||||||
// External Services
|
// External Services
|
||||||
services.AddScoped<IJwtService, JwtService>();
|
services.AddScoped<IJwtService, JwtService>();
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using SPMS.Domain.Entities;
|
||||||
|
using SPMS.Domain.Enums;
|
||||||
|
using SPMS.Domain.Interfaces;
|
||||||
|
|
||||||
|
namespace SPMS.Infrastructure.Persistence.Repositories;
|
||||||
|
|
||||||
|
public class ServiceRepository : Repository<Service>, IServiceRepository
|
||||||
|
{
|
||||||
|
public ServiceRepository(AppDbContext context) : base(context) { }
|
||||||
|
|
||||||
|
public async Task<Service?> GetByServiceCodeAsync(string serviceCode)
|
||||||
|
=> await _dbSet.FirstOrDefaultAsync(s => s.ServiceCode == serviceCode);
|
||||||
|
|
||||||
|
public async Task<Service?> GetByApiKeyAsync(string apiKey)
|
||||||
|
=> await _dbSet.FirstOrDefaultAsync(s => s.ApiKey == apiKey);
|
||||||
|
|
||||||
|
public async Task<Service?> GetByIdWithIpsAsync(long id)
|
||||||
|
=> await _dbSet
|
||||||
|
.Include(s => s.ServiceIps)
|
||||||
|
.FirstOrDefaultAsync(s => s.Id == id);
|
||||||
|
|
||||||
|
public async Task<IReadOnlyList<Service>> GetByStatusAsync(ServiceStatus status)
|
||||||
|
=> await _dbSet.Where(s => s.Status == status).ToListAsync();
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user