웹훅 발송/수신 sample
This commit is contained in:
@@ -0,0 +1,74 @@
|
||||
package com.eactive.eai.rms.data.ext.djb.webhook;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
import org.springframework.data.annotation.CreatedDate;
|
||||
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
|
||||
|
||||
import javax.persistence.*;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Entity
|
||||
@Table(name = "PTL_WEBHOOK_SEND_LOG")
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
@EntityListeners(AuditingEntityListener.class)
|
||||
public class WebhookSendLog {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "webhook_send_log_seq")
|
||||
@SequenceGenerator(
|
||||
name = "webhook_send_log_seq",
|
||||
sequenceName = "SEQ_PTL_WEBHOOK_SEND_LOG",
|
||||
allocationSize = 1
|
||||
)
|
||||
private Long id;
|
||||
|
||||
@Column(name = "TARGET_URL", nullable = false, length = 500)
|
||||
private String targetUrl;
|
||||
|
||||
@Column(name = "EVENT_TYPE", length = 100)
|
||||
private String eventType;
|
||||
|
||||
@Lob
|
||||
@Column(name = "PAYLOAD")
|
||||
private String payload;
|
||||
|
||||
@Column(name = "SIGNATURE", length = 500)
|
||||
private String signature;
|
||||
|
||||
@Column(name = "STATUS_CODE")
|
||||
private Integer statusCode;
|
||||
|
||||
@Lob
|
||||
@Column(name = "RESPONSE_BODY")
|
||||
private String responseBody;
|
||||
|
||||
@Column(name = "SUCCESS", length = 1)
|
||||
private String success; // 오라클 CHAR(1) : 'Y' / 'N'
|
||||
|
||||
@Lob
|
||||
@Column(name = "ERROR_MESSAGE")
|
||||
private String errorMessage;
|
||||
|
||||
@Column(name = "RETRY_COUNT")
|
||||
private Integer retryCount = 0;
|
||||
|
||||
@CreatedDate
|
||||
@Column(name = "CREATED_AT", updatable = false)
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
@Column(name = "SENT_AT")
|
||||
private LocalDateTime sentAt;
|
||||
|
||||
/* Boolean 편의 메서드 */
|
||||
public void setSuccess(Boolean success) {
|
||||
this.success = (success != null && success) ? "Y" : "N";
|
||||
}
|
||||
|
||||
public Boolean getSuccess() {
|
||||
return "Y".equalsIgnoreCase(this.success);
|
||||
}
|
||||
}
|
||||
+131
@@ -0,0 +1,131 @@
|
||||
package com.eactive.eai.rms.ext.djb.webhook.controller;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestHeader;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import com.eactive.eai.rms.data.ext.djb.webhook.WebhookSendLog;
|
||||
import com.eactive.eai.rms.ext.djb.webhook.dto.WebhookSendRequest;
|
||||
import com.eactive.eai.rms.ext.djb.webhook.service.WebhookReceiveService;
|
||||
import com.eactive.eai.rms.ext.djb.webhook.service.WebhookService;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/api/webhook")
|
||||
@RequiredArgsConstructor
|
||||
public class WebhookSendController {
|
||||
|
||||
private final WebhookService webhookService;
|
||||
private final WebhookReceiveService webhookReceiveService;
|
||||
|
||||
private static final int KEY_BYTE_LENGTH = 32; // 256bit
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* 발송 */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
@PostMapping("/send.json")
|
||||
public ResponseEntity<Map<String, Object>> sendWebhook(
|
||||
@RequestBody WebhookSendRequest request) {
|
||||
|
||||
WebhookSendLog result = webhookService.send(
|
||||
request.getTargetUrl(),
|
||||
request.getEventType(),
|
||||
request.getData()
|
||||
);
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("logId", result.getId());
|
||||
response.put("success", result.getSuccess());
|
||||
response.put("statusCode", result.getStatusCode());
|
||||
|
||||
return ResponseEntity.ok(response);
|
||||
}
|
||||
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* 수신 */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
/**
|
||||
* 웹훅 수신 엔드포인트
|
||||
*
|
||||
* 헤더 예시:
|
||||
* X-Webhook-Signature : sha256={hmac값}
|
||||
* X-Webhook-Event : ORDER_CREATED
|
||||
* X-Webhook-Timestamp : 1712345678901
|
||||
*/
|
||||
@PostMapping("/receive")
|
||||
public ResponseEntity<Map<String, Object>> receiveWebhook(
|
||||
@RequestHeader(value = "X-Webhook-Signature", required = false) String signature,
|
||||
@RequestHeader(value = "X-Webhook-Event", required = false) String eventType,
|
||||
@RequestHeader(value = "X-Webhook-Timestamp", required = false) String timestamp,
|
||||
@RequestBody String rawPayload) {
|
||||
|
||||
log.info("[Webhook] 수신 - eventType: {}, timestamp: {}", eventType, timestamp);
|
||||
|
||||
// 1. 필수 헤더 누락 체크
|
||||
if (signature == null || eventType == null || timestamp == null) {
|
||||
log.warn("[Webhook] 수신 거부 - 필수 헤더 누락");
|
||||
return ResponseEntity
|
||||
.status(HttpStatus.BAD_REQUEST)
|
||||
.body(errorResponse("필수 헤더가 누락되었습니다."));
|
||||
}
|
||||
|
||||
// 2. 서명 검증
|
||||
boolean isValid = webhookReceiveService.verifySignature(rawPayload, signature);
|
||||
if (!isValid) {
|
||||
log.warn("[Webhook] 수신 거부 - 서명 불일치 / eventType: {}", eventType);
|
||||
return ResponseEntity
|
||||
.status(HttpStatus.UNAUTHORIZED)
|
||||
.body(errorResponse("서명 검증에 실패하였습니다."));
|
||||
}
|
||||
|
||||
// 3. 타임스탬프 유효성 검증 (5분 이내 요청만 허용)
|
||||
boolean isTimestampValid = webhookReceiveService.verifyTimestamp(timestamp);
|
||||
if (!isTimestampValid) {
|
||||
log.warn("[Webhook] 수신 거부 - 타임스탬프 만료 / eventType: {}", eventType);
|
||||
return ResponseEntity
|
||||
.status(HttpStatus.UNAUTHORIZED)
|
||||
.body(errorResponse("요청이 만료되었습니다."));
|
||||
}
|
||||
|
||||
// 4. 이벤트 처리
|
||||
try {
|
||||
webhookReceiveService.process(eventType, rawPayload);
|
||||
} catch (Exception e) {
|
||||
log.error("[Webhook] 이벤트 처리 실패 - eventType: {}, error: {}", eventType, e.getMessage(), e);
|
||||
return ResponseEntity
|
||||
.status(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||
.body(errorResponse("이벤트 처리 중 오류가 발생하였습니다."));
|
||||
}
|
||||
|
||||
// 5. 정상 응답
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("result", "OK");
|
||||
response.put("eventType", eventType);
|
||||
|
||||
return ResponseEntity.ok(response);
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* 공통 */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
private Map<String, Object> errorResponse(String message) {
|
||||
Map<String, Object> error = new HashMap<>();
|
||||
error.put("result", "ERROR");
|
||||
error.put("message", message);
|
||||
return error;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package com.eactive.eai.rms.ext.djb.webhook.dto;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class WebhookPayload {
|
||||
|
||||
private String eventType;
|
||||
private String eventId;
|
||||
private long timestamp;
|
||||
private Object data;
|
||||
|
||||
public static WebhookPayload of(String eventType, Object data) {
|
||||
return WebhookPayload.builder()
|
||||
.eventType(eventType)
|
||||
.eventId(UUID.randomUUID().toString())
|
||||
.timestamp(System.currentTimeMillis())
|
||||
.data(data)
|
||||
.build();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.eactive.eai.rms.ext.djb.webhook.dto;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
public class WebhookSendRequest {
|
||||
|
||||
private String targetUrl;
|
||||
private String eventType;
|
||||
private Object data;
|
||||
}
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
package com.eactive.eai.rms.ext.djb.webhook.repository;
|
||||
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import com.eactive.eai.rms.data.EMSDataSource;
|
||||
import com.eactive.eai.rms.data.ext.djb.webhook.WebhookSendLog;
|
||||
|
||||
@Repository
|
||||
@EMSDataSource
|
||||
public interface WebhookSendLogRepository extends JpaRepository<WebhookSendLog, Long> {
|
||||
|
||||
// 오라클 CHAR(1) Y/N 기준 조회
|
||||
@Query("SELECT l FROM WebhookSendLog l " +
|
||||
"WHERE l.success = 'N' AND l.retryCount < :maxRetry")
|
||||
List<WebhookSendLog> findFailedLogs(@Param("maxRetry") int maxRetry);
|
||||
|
||||
@Query("SELECT l FROM WebhookSendLog l " +
|
||||
"WHERE l.eventType = :eventType " +
|
||||
"AND l.createdAt BETWEEN :from AND :to")
|
||||
List<WebhookSendLog> findByEventTypeAndPeriod(
|
||||
@Param("eventType") String eventType,
|
||||
@Param("from") LocalDateTime from,
|
||||
@Param("to") LocalDateTime to);
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
package com.eactive.eai.rms.ext.djb.webhook.service;
|
||||
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import javax.crypto.Mac;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class WebhookReceiveService {
|
||||
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
|
||||
private String secretKey = "HW8JtFpkPmQqsVmr0Rb81P4qDypaNIVbGf3ZNMMPfyerhOHshoKABLaMBo1HA4BldTxhw1NjYmVnoPu9IIV7cyRXjecL3b2UctyR7DnVaJausltZLLr8Qm4Bzs4wRmgf";
|
||||
|
||||
private static final String HMAC_ALGORITHM = "HmacSHA256";
|
||||
private static final long TIMESTAMP_LIMIT = 5 * 60 * 1000L; // 5분 (ms)
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* 서명 검증 */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
/**
|
||||
* 수신된 서명과 payload 로 재계산한 서명을 비교
|
||||
* 헤더값 형식 : "sha256={hex값}"
|
||||
*/
|
||||
public boolean verifySignature(String rawPayload, String receivedSignature) {
|
||||
try {
|
||||
// "sha256=" 접두어 제거
|
||||
String receivedHex = receivedSignature.startsWith("sha256=")
|
||||
? receivedSignature.substring(7)
|
||||
: receivedSignature;
|
||||
|
||||
String expectedHex = generateSignature(rawPayload);
|
||||
|
||||
// 타이밍 공격 방지 : MessageDigest.isEqual 사용
|
||||
return MessageDigest.isEqual(
|
||||
expectedHex.getBytes(StandardCharsets.UTF_8),
|
||||
receivedHex.getBytes(StandardCharsets.UTF_8)
|
||||
);
|
||||
} catch (Exception e) {
|
||||
log.error("[Webhook] 서명 검증 중 오류 발생", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* 타임스탬프 검증 */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
/**
|
||||
* 요청 타임스탬프가 현재 시각 기준 5분 이내인지 확인 (Replay Attack 방지)
|
||||
*/
|
||||
public boolean verifyTimestamp(String timestampStr) {
|
||||
try {
|
||||
long requestTime = Long.parseLong(timestampStr);
|
||||
long now = System.currentTimeMillis();
|
||||
return Math.abs(now - requestTime) <= TIMESTAMP_LIMIT;
|
||||
} catch (NumberFormatException e) {
|
||||
log.warn("[Webhook] 타임스탬프 파싱 실패: {}", timestampStr);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* 이벤트 처리 */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
/**
|
||||
* eventType 에 따라 분기 처리
|
||||
*/
|
||||
public void process(String eventType, String rawPayload) throws Exception {
|
||||
JsonNode root = objectMapper.readTree(rawPayload);
|
||||
JsonNode data = root.path("data");
|
||||
|
||||
log.info("[Webhook] 이벤트 처리 시작 - eventType: {}", eventType);
|
||||
|
||||
switch (eventType) {
|
||||
case "ORDER_CREATED":
|
||||
handleOrderCreated(data);
|
||||
break;
|
||||
case "ORDER_CANCELLED":
|
||||
handleOrderCancelled(data);
|
||||
break;
|
||||
case "PAYMENT_COMPLETED":
|
||||
handlePaymentCompleted(data);
|
||||
break;
|
||||
default:
|
||||
log.warn("[Webhook] 처리되지 않은 이벤트 - eventType: {}", eventType);
|
||||
break;
|
||||
}
|
||||
|
||||
log.info("[Webhook] 이벤트 처리 완료 - eventType: {}", eventType);
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* 이벤트별 핸들러 (비즈니스 로직 구현) */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
private void handleOrderCreated(JsonNode data) {
|
||||
log.info("[Webhook] ORDER_CREATED 처리 - data: {}", data);
|
||||
// TODO: 주문 생성 처리 로직
|
||||
}
|
||||
|
||||
private void handleOrderCancelled(JsonNode data) {
|
||||
log.info("[Webhook] ORDER_CANCELLED 처리 - data: {}", data);
|
||||
// TODO: 주문 취소 처리 로직
|
||||
}
|
||||
|
||||
private void handlePaymentCompleted(JsonNode data) {
|
||||
log.info("[Webhook] PAYMENT_COMPLETED 처리 - data: {}", data);
|
||||
// TODO: 결제 완료 처리 로직
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ */
|
||||
/* 공통 유틸 */
|
||||
/* ------------------------------------------------------------------ */
|
||||
|
||||
private String generateSignature(String payload) throws Exception {
|
||||
Mac mac = Mac.getInstance(HMAC_ALGORITHM);
|
||||
SecretKeySpec keySpec = new SecretKeySpec(
|
||||
secretKey.getBytes(StandardCharsets.UTF_8), HMAC_ALGORITHM);
|
||||
mac.init(keySpec);
|
||||
byte[] hash = mac.doFinal(payload.getBytes(StandardCharsets.UTF_8));
|
||||
return bytesToHex(hash);
|
||||
}
|
||||
|
||||
private String bytesToHex(byte[] bytes) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (byte b : bytes) {
|
||||
sb.append(String.format("%02x", b));
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
package com.eactive.eai.rms.ext.djb.webhook.service;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
import javax.crypto.Mac;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.http.HttpEntity;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.web.client.HttpClientErrorException;
|
||||
import org.springframework.web.client.HttpServerErrorException;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
import com.eactive.eai.rms.data.ext.djb.webhook.WebhookSendLog;
|
||||
import com.eactive.eai.rms.ext.djb.webhook.dto.WebhookPayload;
|
||||
import com.eactive.eai.rms.ext.djb.webhook.repository.WebhookSendLogRepository;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class WebhookService {
|
||||
|
||||
private final WebhookSendLogRepository sendLogRepository;
|
||||
private final ObjectMapper objectMapper;
|
||||
private final RestTemplate restTemplate;
|
||||
|
||||
|
||||
private String secretKey = "HW8JtFpkPmQqsVmr0Rb81P4qDypaNIVbGf3ZNMMPfyerhOHshoKABLaMBo1HA4BldTxhw1NjYmVnoPu9IIV7cyRXjecL3b2UctyR7DnVaJausltZLLr8Qm4Bzs4wRmgf";
|
||||
|
||||
private static final String HMAC_ALGORITHM = "HmacSHA256";
|
||||
private static final int MAX_RETRY = 3;
|
||||
|
||||
/**
|
||||
* 웹훅 발송 메인 메서드
|
||||
*/
|
||||
@Transactional
|
||||
public WebhookSendLog send(String targetUrl, String eventType, Object data) {
|
||||
WebhookSendLog sendLog = new WebhookSendLog();
|
||||
sendLog.setTargetUrl(targetUrl);
|
||||
sendLog.setEventType(eventType);
|
||||
|
||||
try {
|
||||
// 1. Payload 생성
|
||||
WebhookPayload webhookPayload = WebhookPayload.of(eventType, data);
|
||||
String payloadJson = objectMapper.writeValueAsString(webhookPayload);
|
||||
sendLog.setPayload(payloadJson);
|
||||
|
||||
// 2. Secret Key로 HMAC-SHA256 서명 생성
|
||||
String signature = generateSignature(payloadJson);
|
||||
sendLog.setSignature(signature);
|
||||
|
||||
// 3. HTTP 요청 헤더 구성
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setContentType(MediaType.APPLICATION_JSON);
|
||||
headers.set("X-Webhook-Signature", "sha256=" + signature);
|
||||
headers.set("X-Webhook-Event", eventType);
|
||||
headers.set("X-Webhook-Timestamp", String.valueOf(webhookPayload.getTimestamp()));
|
||||
|
||||
// 4. 발송
|
||||
sendLog.setSentAt(LocalDateTime.now());
|
||||
HttpEntity<String> request = new HttpEntity<>(payloadJson, headers);
|
||||
ResponseEntity<String> response = restTemplate.postForEntity(targetUrl, request, String.class);
|
||||
|
||||
// 5. 성공 로그
|
||||
sendLog.setStatusCode(response.getStatusCode().value());
|
||||
sendLog.setResponseBody(response.getBody());
|
||||
sendLog.setSuccess(response.getStatusCode().is2xxSuccessful());
|
||||
|
||||
log.info("[Webhook] 발송 성공 - eventType: {}, url: {}, status: {}",
|
||||
eventType, targetUrl, response.getStatusCode());
|
||||
|
||||
} catch (HttpClientErrorException | HttpServerErrorException e) {
|
||||
sendLog.setStatusCode(e.getStatusCode().value());
|
||||
sendLog.setResponseBody(e.getResponseBodyAsString());
|
||||
sendLog.setSuccess(false);
|
||||
sendLog.setErrorMessage(e.getMessage());
|
||||
log.error("[Webhook] HTTP 오류 - eventType: {}, url: {}, status: {}",
|
||||
eventType, targetUrl, e.getStatusCode());
|
||||
|
||||
} catch (Exception e) {
|
||||
sendLog.setSuccess(false);
|
||||
sendLog.setErrorMessage(e.getMessage());
|
||||
log.error("[Webhook] 발송 실패 - eventType: {}, url: {}, error: {}",
|
||||
eventType, targetUrl, e.getMessage());
|
||||
}
|
||||
|
||||
return sendLogRepository.save(sendLog);
|
||||
}
|
||||
|
||||
/**
|
||||
* 실패 건 재시도
|
||||
*/
|
||||
@Transactional
|
||||
public void retryFailedWebhooks() {
|
||||
List<WebhookSendLog> failedLogs =
|
||||
sendLogRepository.findFailedLogs(MAX_RETRY);
|
||||
|
||||
for (WebhookSendLog failedLog : failedLogs) {
|
||||
try {
|
||||
log.info("[Webhook] 재시도 - id: {}, retry: {}", failedLog.getId(), failedLog.getRetryCount());
|
||||
failedLog.setRetryCount(failedLog.getRetryCount() + 1);
|
||||
sendLogRepository.save(failedLog);
|
||||
|
||||
send(failedLog.getTargetUrl(), failedLog.getEventType(),
|
||||
objectMapper.readValue(failedLog.getPayload(), Object.class));
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("[Webhook] 재시도 실패 - id: {}", failedLog.getId(), e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* HMAC-SHA256 서명 생성
|
||||
* Java 17 미만은 HexFormat 미지원이므로 직접 변환
|
||||
*/
|
||||
private String generateSignature(String payload) throws Exception {
|
||||
Mac mac = Mac.getInstance(HMAC_ALGORITHM);
|
||||
SecretKeySpec keySpec = new SecretKeySpec(
|
||||
secretKey.getBytes(StandardCharsets.UTF_8), HMAC_ALGORITHM);
|
||||
mac.init(keySpec);
|
||||
byte[] hash = mac.doFinal(payload.getBytes(StandardCharsets.UTF_8));
|
||||
return bytesToHex(hash);
|
||||
}
|
||||
|
||||
private String bytesToHex(byte[] bytes) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (byte b : bytes) {
|
||||
sb.append(String.format("%02x", b));
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user