개발 가이드 · Webhook · HMAC-SHA256

웹훅 개발가이드

DJBank가 발송하는 Webhook 요청의 진위를 확인하기 위한 HMAC-SHA256 서명 검증 방법을 단계별로 설명합니다.

OVERVIEW

서명 검증이 필요한 이유

1

Secret 확보

[마이페이지 > Webhook 관리]에서 발급된

Secret Key를 서버에 안전하게 보관.

2

원문(raw body) 보존

수신 즉시 본문을 파싱/재직렬화하지 말고

바이트 원문 그대로 서명 계산에 사용.

3

HMAC 재계산·비교

동일 Secret으로 HMAC-SHA256을 재계산해

헤더 서명과 상수 시간으로 비교.

SEQUENCE

전체 서명·검증 시퀀스

DJBank Webhook Sender Your Endpoint ① 이벤트 발생 (점검·지연·장애) ② signature = HMAC-SHA256(secret, body) ③ POST body · X-Webhook-Signature: sha256=<hex> ④ 동일 secret으로 재계산 ⑤ 상수 시간 비교 (일치 여부) ⑥ 200 OK (검증 성공 시)
STEP 1

수신 요청 형식

DJBank는 등록한 수신 URL로 아래 형태의 POST 요청을 전송합니다.

POST https://your-service.example.com/webhook application/json

요청 헤더

HEADER 설명
X-Webhook-Signature sha256=<hex> — 본문 HMAC-SHA256 서명(소문자 hex)
X-Webhook-Event 이벤트 코드 (예: CONTROL_START)
X-Webhook-Timestamp 발송 시각 (epoch millis) — 재전송 방어용
Content-Type application/json

⚠ 서명 대상은 파싱 전 본문 원문(raw body) 입니다.

Request Body · JSON
{
  "eventType": "CONTROL_START",
  "eventId": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
  "timestamp": 1723600000000,
  "data": [ "TESTCASE003S1", "TESTCASE005S1" ]
}

# eventId: 발송 건 고유 ID · data: 영향 API 목록
STEP 2

서명 검증 알고리즘

수신한 본문 원문과 발급된 Secret으로 서명을 재계산해 헤더 값과 비교합니다.

Verification Steps
# 1) 헤더에서 서명 추출
received = header["X-Webhook-Signature"]   # "sha256=...."

# 2) 본문 원문으로 HMAC-SHA256 재계산
digest   = HMAC_SHA256(secret, rawBody)     # bytes
expected = "sha256=" + toHexLower(digest)

# 3) 상수 시간 비교
valid    = constantTimeEquals(received, expected)

검증 규칙

  • 알고리즘: HmacSHA256, 키: Secret(UTF-8 bytes)
  • 메시지: 수신 본문 원문(UTF-8 bytes)
  • 출력: 소문자 hex, 헤더는 sha256= 접두 포함
  • 비교: 타이밍 공격 방지를 위해 상수 시간 비교
  • 불일치 시 요청을 폐기하고 2xx 이외로 응답

재전송(replay) 방어

  • X-Webhook-Timestamp가 허용 오차(예: 5분) 밖이면 거부
  • 동일 eventId 중복 수신은 멱등 처리
STEP 3

언어별 서명 검증 예제

프레임워크에서 반드시 원문 바디에 접근할 수 있어야 합니다.

Node.js (Express)
const crypto = require('crypto');

// rawBody: express.raw() 등으로 확보한 원문 Buffer
function verify(rawBody, header, secret) {
  const expected = 'sha256=' +
    crypto.createHmac('sha256', secret)
          .update(rawBody)
          .digest('hex');
  const a = Buffer.from(header);
  const b = Buffer.from(expected);
  return a.length === b.length &&
         crypto.timingSafeEqual(a, b);
}
Python (Flask)
import hmac, hashlib

# raw_body: request.get_data() 로 확보한 bytes
def verify(raw_body, header, secret):
    digest = hmac.new(
        secret.encode('utf-8'),
        raw_body,
        hashlib.sha256
    ).hexdigest()
    expected = 'sha256=' + digest
    return hmac.compare_digest(expected, header)
Java
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;

// rawBody: 파싱 전 원문 문자열
boolean verify(String rawBody, String header, String secret) throws Exception {
  Mac mac = Mac.getInstance("HmacSHA256");
  mac.init(new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
  byte[] hash = mac.doFinal(rawBody.getBytes(StandardCharsets.UTF_8));
  StringBuilder hex = new StringBuilder();
  for (byte b : hash) hex.append(String.format("%02x", b));
  String expected = "sha256=" + hex;
  return MessageDigest.isEqual(
      expected.getBytes(StandardCharsets.UTF_8),
      header.getBytes(StandardCharsets.UTF_8));
}

이벤트 코드 (X-Webhook-Event)

CODE 의미
CONTROL_START 점검 시작
CONTROL_END 점검 종료
DELAY_START 지연 시작
DELAY_END 지연 종료
ERROR_START 장애 발생
ERROR_END 장애 종료
STEP 4

응답(리턴) 반환 규칙

수신 서버가 반환하는 HTTP 상태 코드에 따라 DJBank의 성공 판정과 재시도가 결정됩니다.

상태 코드별 처리

반환 상황 DJBank 처리
2xx 검증 성공 + 정상 접수 발송 성공 기록. 재시도 없음
400 필수 헤더 누락 실패 기록
401 서명 불일치 / timestamp 만료 실패 기록
5xx · 타임아웃 수신 서버 일시 장애 실패 기록 + 재시도

⚠ 5xx 응답·타임아웃과 네트워크 오류는 일정 간격을 두고 재시도됩니다(기본 3회). 재시도로 인한 중복 수신은 eventId 멱등 처리로 방어하세요.

>>>>>>> 9981459691c836bfd33b6b81a8e6aa22ca12446a

응답 가이드

  • 검증 통과 시 즉시 200 OK 반환 — 무거운 후속 처리는 비동기로 분리
  • 응답 본문 규격은 자유(발송 로그에 기록만 됨) — 간단한 JSON 권장
  • 서명 검증 실패는 401, 필수 헤더 누락은 400 반환 권장
  • 동일 eventId 재수신 시 재처리 없이 200 반환(멱등)
200 OK · JSON (권장)
# 정상 접수
HTTP/1.1 200 OK
Content-Type: application/json

{
  "result": "OK",
  "eventType": "CONTROL_START"
}

# 서명 검증 실패
HTTP/1.1 401 Unauthorized
{
  "result": "ERROR",
  "message": "서명 검증에 실패하였습니다."
}

# 필수 헤더 누락
HTTP/1.1 400 Bad Request
{
  "result": "ERROR",
  "message": "필수 헤더가 누락되었습니다."
}
TROUBLESHOOTING

서명 불일치 대표 원인

원인 증상 해결 가이드
본문 재직렬화 JSON 파싱 후 다시 문자열화한 값으로 서명 계산 파싱 전 raw body(bytes)로 계산
hex 대소문자 대문자 hex로 비교해 불일치 소문자 hex 사용
접두어 처리 sha256= 접두 포함/제외 불일치 양쪽 모두 접두 포함 후 비교
인코딩 Secret/본문을 UTF-8 외로 인코딩 키·메시지 모두 UTF-8 bytes
Secret 불일치 재발급 후 이전 Secret 사용 최신 Secret으로 교체
재전송 동일 이벤트 중복 수신 timestamp 검사 + eventId 멱등 처리
MANAGE

Webhook 관리로 가기

수신 URL·Secret·구독 이벤트를 등록하고 관리하세요.

Webhook 관리 →