웹훅 완벽 가이드: 서명된 페이로드와 재시도 전략
API를 구축했고 이제 이벤트가 발생할 때 다른 서비스에 알려야 합니다. 폴링은 비효율적이므로 웹훅을 구현합니다. 그러나 곧 두 가지 일반적인 문제에 직면합니다: 페이로드가 변조되지 않았음을 어떻게 보장할 것인가, 그리고 수신 엔드포인트가 다운되었을 때 어떻게 할 것인가. 이 글에서는 서명된 페이로드와 재시도 전략을 사용하여 웹훅을 안전하고 안정적으로 만드는 방법을 설명합니다.
웹훅에 서명과 재시도가 필요한 이유
웹훅은 이벤트에 의해 트리거되는 HTTP 콜백입니다. 구현은 간단하지만 보안 및 안정성 문제를 야기합니다. 서명이 없으면 공격자가 엔드포인트에 요청을 위조하여 데이터 손상이나 권한 없는 작업을 초래할 수 있습니다. 재시도가 없으면 일시적인 실패(예: 네트워크 장애 또는 일시적인 서버 오류)로 인해 이벤트를 놓치게 됩니다.
서명된 페이로드는 진위성을 해결합니다: 발신자가 비밀 키로 페이로드에 서명하고 수신자가 이를 검증합니다. 재시도는 안정성을 해결합니다: 발신자가 실패한 전달을 백오프와 함께 재시도합니다.
서명된 페이로드의 작동 방식
서명된 페이로드에는 요청 본문과 공유 비밀에서 생성된 암호화 서명이 포함됩니다. 가장 일반적인 방법은 SHA-256을 사용하는 HMAC(해시 기반 메시지 인증 코드)입니다.
서명 생성
발신자는 비밀 키를 사용하여 원시 요청 본문의 HMAC을 계산합니다. 서명은 종종 재생 공격을 방지하기 위해 타임스탬프와 함께 헤더로 전송됩니다.
// Node.js 예제: HMAC 서명 생성
const crypto = require('crypto');
function signPayload(payload, secret) {
const timestamp = Math.floor(Date.now() / 1000);
const signedPayload = `${timestamp}.${JSON.stringify(payload)}`;
const signature = crypto
.createHmac('sha256', secret)
.update(signedPayload)
.digest('hex');
return { timestamp, signature };
}
타임스탬프와 서명을 헤더에 포함하세요. 예: X-Webhook-Timestamp 및 X-Webhook-Signature.
서명 검증
수신자는 수신된 타임스탬프와 원시 본문을 사용하여 서명된 페이로드를 재구성한 다음 HMAC을 계산하고 안전하게 비교해야 합니다.
// Node.js 예제: 서명 검증
function verifySignature(rawBody, timestamp, receivedSignature, secret) {
const signedPayload = `${timestamp}.${rawBody}`;
const expectedSignature = crypto
.createHmac('sha256', secret)
.update(signedPayload)
.digest('hex');
// 타이밍 안전 비교 사용
return crypto.timingSafeEqual(
Buffer.from(expectedSignature, 'hex'),
Buffer.from(receivedSignature, 'hex')
);
}
타이밍 공격을 방지하려면 항상 타이밍 안전 비교를 사용하세요. 또한 재생 공격을 완화하려면 타임스탬프가 최근인지(예: 5분 이내) 확인하세요.
재시도 전략 설계
재시도는 안정적인 웹훅 전달에 필수적입니다. 그러나 순진한 재시도는 수신자를 압도하거나 중복 처리를 유발할 수 있습니다. 좋은 재시도 전략에는 다음이 포함됩니다:
- 지수 백오프: 각 재시도 사이에 더 오래 기다립니다(예: 1초, 2초, 4초, 8초...).
- 최대 재시도 횟수: 무한 루프를 피하기 위해 시도 횟수를 제한합니다(예: 5회).
- 데드 레터 큐: 최대 재시도 후 수동 검사를 위해 이벤트를 저장합니다.
- 멱등성: 수신자가 중복 제거할 수 있도록 고유한 이벤트 ID를 포함합니다.
다음은 의사 코드로 작성된 간단한 재시도 루프입니다:
function deliverWithRetry(url, payload, maxRetries = 5) {
let attempt = 0;
while (attempt < maxRetries) {
try {
const response = await fetch(url, { method: 'POST', body: payload });
if (response.ok) return;
} catch (error) {
// 오류 로깅
}
attempt++;
const delay = Math.pow(2, attempt) * 1000; // 지수 백오프
await new Promise(resolve => setTimeout(resolve, delay));
}
// 데드 레터 큐로 이동
}
썬더링 허드 문제를 피하기 위해 백오프에 지터를 추가하는 것을 고려하세요.
비교: 폴링 vs 재시도가 있는 웹훅
| 측면 | 폴링 | 재시도가 있는 웹훅 |
|---|---|---|
| 지연 시간 | 높음 (간격에 따라 다름) | 낮음 (거의 실시간) |
| 서버 부하 | 일정함 (이벤트가 없을 때도) | 이벤트 기반 |
| 안정성 | 높음 (수신자가 제어) | 재시도 전략에 따라 다름 |
| 보안 | 표준 API 인증 | 서명된 페이로드 필요 |
단계별: 서명과 재시도를 사용한 웹훅 구현
- 각 웹훅 엔드포인트에 대한 비밀 키 생성 및 안전하게 공유.
- 이벤트 발생 시, 페이로드(JSON)를 구성하고 비밀과 타임스탬프를 사용하여 HMAC-SHA256으로 서명을 계산.
- POST 요청 전송 시 헤더 포함:
Content-Type: application/json,X-Webhook-Timestamp,X-Webhook-Signature. - 수신자 측에서, 원시 본문과 타임스탬프를 사용하여 서명을 검증. 유효하지 않거나 타임스탬프가 너무 오래된 경우 거부.
- 전달 실패 시 (비 2xx 응답 또는 네트워크 오류), 최대 시도 횟수까지 지수 백오프로 재시도.
- 최대 재시도 후, 실패를 기록하고 선택적으로 경고; 수동 재생을 위해 이벤트를 데드 레터 큐에 저장.
- 수신자 측에서 멱등성 보장을 위해 이벤트 ID를 사용하여 중복 무시.
일반적인 함정 및 모범 사례
- 원시 본문을 사용하지 않음: 많은 프레임워크가 JSON을 파싱하고 본문을 변경합니다. 서명 검증에는 원시 본문을 사용하세요.
- 타임스탬프 무시: 타임스탬프 검증이 없으면 재생 공격이 가능합니다.
- 멱등성 없음: 재시도로 인해 중복 처리가 발생할 수 있습니다. 멱등성 키를 사용하세요.
- 무한 재시도: 항상 최대 재시도 제한과 데드 레터 큐를 설정하세요.
- 로깅 없음: 디버깅을 위해 전달 시도와 실패를 기록하세요.
FAQ
서명된 웹훅 페이로드란 무엇인가요?
서명된 웹훅 페이로드에는 암호화 서명(예: HMAC)이 포함되어 있어 수신자가 요청이 예상 발신자로부터 시작되었고 변조되지 않았음을 확인할 수 있습니다.
실패한 웹훅을 몇 번 재시도해야 하나요?
일반적인 관행은 지수 백오프를 사용하여 3~5회 재시도하는 것입니다. 정확한 횟수는 SLA와 이벤트의 중요성에 따라 다릅니다. 재시도 제한을 초과한 이벤트에는 항상 데드 레터 큐를 사용하세요.
서명 없이 웹훅을 사용할 수 있나요?
권장하지 않습니다. 서명이 없으면 엔드포인트가 위조 요청에 취약합니다. 항상 서명(예: HMAC)을 사용하고 재생 공격을 방지하기 위해 타임스탬프를 검증하세요.
웹훅 전달을 디버깅할 때 서버 로그를 검사하는 것이 중요합니다. Nginx를 사용하는 경우 Nginx Log Analyzer를 사용하여 액세스 로그를 구문 분석하고 웹훅 요청을 추적하며 실패를 식별할 수 있습니다.