Secret 확보
[마이페이지 > Webhook 관리]에서 발급된
Secret Key를 서버에 안전하게 보관.
DJBank가 발송하는 Webhook 요청의 진위를 확인하기 위한 HMAC-SHA256 서명 검증 방법을 단계별로 설명합니다.
[마이페이지 > Webhook 관리]에서 발급된
Secret Key를 서버에 안전하게 보관.
수신 즉시 본문을 파싱/재직렬화하지 말고
바이트 원문 그대로 서명 계산에 사용.
동일 Secret으로 HMAC-SHA256을 재계산해
헤더 서명과 상수 시간으로 비교.
DJBank는 등록한 수신 URL로 아래 형태의 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) 입니다.
{
"eventType": "CONTROL_START",
"eventId": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
"timestamp": 1723600000000,
"data": [ "TESTCASE003S1", "TESTCASE005S1" ]
}
# eventId: 발송 건 고유 ID · data: 영향 API 목록
수신한 본문 원문과 발급된 Secret으로 서명을 재계산해 헤더 값과 비교합니다.
# 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)sha256= 접두 포함X-Webhook-Timestamp가 허용 오차(예: 5분) 밖이면 거부eventId 중복 수신은 멱등 처리프레임워크에서 반드시 원문 바디에 접근할 수 있어야 합니다.
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); }
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)
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)); }
| CODE | 의미 |
|---|---|
CONTROL_START |
점검 시작 |
CONTROL_END |
점검 종료 |
DELAY_START |
지연 시작 |
DELAY_END |
지연 종료 |
ERROR_START |
장애 발생 |
ERROR_END |
장애 종료 |
수신 서버가 반환하는 HTTP 상태 코드에 따라 DJBank의 성공 판정과 재시도가 결정됩니다.
| 반환 | 상황 | DJBank 처리 |
|---|---|---|
| 2xx | 검증 성공 + 정상 접수 | 발송 성공 기록. 재시도 없음 |
400 |
필수 헤더 누락 | 실패 기록 |
401 |
서명 불일치 / timestamp 만료 | 실패 기록 |
5xx · 타임아웃 |
수신 서버 일시 장애 | 실패 기록 + 재시도 |
⚠ 5xx 응답·타임아웃과 네트워크 오류는 일정 간격을 두고
재시도됩니다(기본 3회). 재시도로 인한 중복 수신은 eventId 멱등 처리로 방어하세요.
401, 필수 헤더 누락은 400 반환 권장eventId 재수신 시 재처리 없이 200 반환(멱등)# 정상 접수 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": "필수 헤더가 누락되었습니다." }
| 원인 | 증상 | 해결 가이드 |
|---|---|---|
| 본문 재직렬화 | JSON 파싱 후 다시 문자열화한 값으로 서명 계산 | 파싱 전 raw body(bytes)로 계산 |
| hex 대소문자 | 대문자 hex로 비교해 불일치 | 소문자 hex 사용 |
| 접두어 처리 | sha256= 접두 포함/제외 불일치 |
양쪽 모두 접두 포함 후 비교 |
| 인코딩 | Secret/본문을 UTF-8 외로 인코딩 | 키·메시지 모두 UTF-8 bytes |
| Secret 불일치 | 재발급 후 이전 Secret 사용 | 최신 Secret으로 교체 |
| 재전송 | 동일 이벤트 중복 수신 | timestamp 검사 + eventId 멱등 처리 |
수신 URL·Secret·구독 이벤트를 등록하고 관리하세요.