From 351135549e2e9e198c2e42549164484535b5585c Mon Sep 17 00:00:00 2001 From: SEAN Date: Wed, 25 Feb 2026 13:56:59 +0900 Subject: [PATCH] =?UTF-8?q?improvement:=20API=20Key=20=EB=A7=88=EC=8A=A4?= =?UTF-8?q?=ED=82=B9=20=EB=B0=8F=20=EC=A0=84=EC=B2=B4=20=EC=A1=B0=ED=9A=8C?= =?UTF-8?q?=20=EC=97=94=EB=93=9C=ED=8F=AC=EC=9D=B8=ED=8A=B8=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20(#220)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 상세 조회 시 API Key 마스킹 (앞 8자 + ********) - API Key 전체 조회 엔드포인트 신규 (apikey/view) - 기존 재발급 엔드포인트 (apikey/refresh) 유지 - Swagger Description 업데이트 Closes #220 --- SPMS.API/Controllers/ServiceController.cs | 16 ++++++++- .../Interfaces/IServiceManagementService.cs | 1 + .../Services/ServiceManagementService.cs | 35 ++++++++++++++++++- 3 files changed, 50 insertions(+), 2 deletions(-) diff --git a/SPMS.API/Controllers/ServiceController.cs b/SPMS.API/Controllers/ServiceController.cs index fc968a7..ddb0ed0 100644 --- a/SPMS.API/Controllers/ServiceController.cs +++ b/SPMS.API/Controllers/ServiceController.cs @@ -116,7 +116,7 @@ public class ServiceController : ControllerBase [HttpPost("{serviceCode}")] [SwaggerOperation( Summary = "서비스 상세 조회", - Description = "특정 서비스의 상세 정보를 조회합니다. apnsAuthType(p8/p12)과 platforms 필드로 각 플랫폼의 자격증명 상태(credentialStatus: ok/warn/error), 만료일(expiresAt) 정보를 포함합니다.")] + Description = "특정 서비스의 상세 정보를 조회합니다. API Key는 마스킹(앞 8자+********)되어 반환되며, 전체 키 조회는 apikey/view 엔드포인트를 사용합니다. apnsAuthType(p8/p12)과 platforms 필드로 각 플랫폼의 자격증명 상태(credentialStatus: ok/warn/error), 만료일(expiresAt) 정보를 포함합니다.")] [SwaggerResponse(200, "조회 성공", typeof(ApiResponse))] [SwaggerResponse(401, "인증되지 않은 요청")] [SwaggerResponse(403, "권한 없음")] @@ -144,6 +144,20 @@ public class ServiceController : ControllerBase return Ok(ApiResponse.Success(result)); } + [HttpPost("{serviceCode}/apikey/view")] + [SwaggerOperation( + Summary = "API Key 전체 조회", + Description = "서비스의 API Key 전체 값을 조회합니다. 상세 조회 시 마스킹된 키 대신 전체 키를 확인할 때 사용합니다. 키 회전(재발급) 없이 현재 키를 반환합니다.")] + [SwaggerResponse(200, "조회 성공", typeof(ApiResponse))] + [SwaggerResponse(401, "인증되지 않은 요청")] + [SwaggerResponse(403, "권한 없음")] + [SwaggerResponse(404, "서비스를 찾을 수 없음")] + public async Task ViewApiKeyAsync([FromRoute] string serviceCode) + { + var result = await _serviceManagementService.ViewApiKeyAsync(serviceCode); + return Ok(ApiResponse.Success(result)); + } + [HttpPost("{serviceCode}/apikey/refresh")] [SwaggerOperation( Summary = "API Key 재발급", diff --git a/SPMS.Application/Interfaces/IServiceManagementService.cs b/SPMS.Application/Interfaces/IServiceManagementService.cs index cee25ab..801275d 100644 --- a/SPMS.Application/Interfaces/IServiceManagementService.cs +++ b/SPMS.Application/Interfaces/IServiceManagementService.cs @@ -12,6 +12,7 @@ public interface IServiceManagementService Task GetByServiceCodeAsync(string serviceCode); Task DeleteAsync(DeleteServiceRequestDto request); Task ChangeStatusAsync(string serviceCode, ChangeServiceStatusRequestDto request); + Task ViewApiKeyAsync(string serviceCode); Task RefreshApiKeyAsync(string serviceCode); Task RegisterApnsCredentialsAsync(string serviceCode, ApnsCredentialsRequestDto request); Task RegisterFcmCredentialsAsync(string serviceCode, FcmCredentialsRequestDto request); diff --git a/SPMS.Application/Services/ServiceManagementService.cs b/SPMS.Application/Services/ServiceManagementService.cs index 1822be2..a2eee0b 100644 --- a/SPMS.Application/Services/ServiceManagementService.cs +++ b/SPMS.Application/Services/ServiceManagementService.cs @@ -314,6 +314,25 @@ public class ServiceManagementService : IServiceManagementService return MapToDto(service); } + public async Task ViewApiKeyAsync(string serviceCode) + { + var service = await _serviceRepository.GetByServiceCodeAsync(serviceCode); + if (service is null) + { + throw new SpmsException( + ErrorCodes.NotFound, + "서비스를 찾을 수 없습니다.", + 404); + } + + return new ApiKeyRefreshResponseDto + { + ServiceCode = service.ServiceCode, + ApiKey = service.ApiKey, + ApiKeyCreatedAt = service.ApiKeyCreatedAt + }; + } + public async Task RefreshApiKeyAsync(string serviceCode) { var service = await _serviceRepository.GetByServiceCodeAsync(serviceCode); @@ -981,7 +1000,7 @@ public class ServiceManagementService : IServiceManagementService ServiceCode = service.ServiceCode, ServiceName = service.ServiceName, Description = service.Description, - ApiKey = service.ApiKey, + ApiKey = MaskApiKey(service.ApiKey), ApiKeyCreatedAt = service.ApiKeyCreatedAt, ApnsBundleId = service.ApnsBundleId, ApnsKeyId = service.ApnsKeyId, @@ -1003,6 +1022,20 @@ public class ServiceManagementService : IServiceManagementService }; } + /// + /// API Key를 마스킹합니다 (앞 8자 노출 + ********). + /// + private static string MaskApiKey(string apiKey) + { + if (string.IsNullOrEmpty(apiKey)) + return string.Empty; + + if (apiKey.Length <= 8) + return new string('*', apiKey.Length); + + return apiKey[..8] + "********"; + } + /// /// 서비스의 플랫폼 자격증명 상태를 판정하여 PlatformSummaryDto를 반환합니다. /// Android: FcmCredentials 유무로 판정 -- 2.45.1