웹훅 완벽 가이드: 서명된 페이로드와 재시도
웹훅이 까다로운 이유
웹훅은 간단한 아이디어입니다. API를 폴링하여 변경 사항을 확인하는 대신, 제공자가 HTTP POST를 통해 이벤트를 엔드포인트로 푸시합니다. 하지만 실제로는 많은 개발자를 곤란하게 만드는 여러 가지 문제가 있습니다. 요청이 실제로 제공자로부터 온 것인지 검증하고, 중복 전달을 처리하고, 네트워크 장애에 대응해야 합니다. 이를 무시하면 보안 침해, 데이터 손상, 사용자 불만을 초래할 수 있습니다.
이 글에서는 프로덕션 수준의 웹훅에서 가장 중요한 두 가지 측면인 서명된 페이로드와 재시도를 살펴봅니다. 서명 검증 방법, 지수 백오프를 사용한 재시도 구현, 그리고 처리를 멱등하게 만드는 방법을 다룹니다.
서명된 페이로드: 진위 증명
누구나 여러분의 엔드포인트로 HTTP POST를 보낼 수 있습니다. 요청이 제공자로부터 온 것임을 보장하려면, 제공자가 비밀 키로 페이로드에 서명하고 서명을 헤더에 담아 보냅니다. 여러분의 역할은 서명을 다시 계산하여 비교하는 것입니다.
대부분의 제공자는 HMAC-SHA256을 사용합니다. 서명은 일반적으로 원시 요청 본문(때로는 타임스탬프 포함)에 대해 계산되어 16진수로 인코딩됩니다. 예를 들어 Stripe는 타임스탬프와 서명을 포함하는 Stripe-Signature와 같은 헤더를 사용합니다.
HMAC의 작동 방식
HMAC(해시 기반 메시지 인증 코드)은 비밀 키와 해시 함수를 결합합니다. 동일한 키와 메시지가 주어지면 출력은 결정적입니다. 키 없이는 공격자가 유효한 서명을 위조할 수 없습니다.
다음은 Node.js의 최소 예제입니다.
const crypto = require('crypto');
function verifySignature(payload, signature, secret) {
const expected = crypto
.createHmac('sha256', secret)
.update(payload)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expected)
);
}
Python에서도 마찬가지로 간단합니다.
import hmac
import hashlib
def verify_signature(payload: bytes, signature: str, secret: str) -> bool:
expected = hmac.new(
secret.encode(),
payload,
hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, signature)
서명 검증 모범 사례
- 상수 시간 비교를 사용하여 타이밍 공격을 방지하세요.
crypto.timingSafeEqual과hmac.compare_digest모두 안전합니다. - 원시 본문을 검증하세요. 파싱된 JSON 객체가 아닙니다. 파싱 후 재직렬화하면 공백이 변경되어 서명이 깨질 수 있습니다.
- 재전송 공격을 방지하려면 서명된 데이터에 타임스탬프를 포함하세요. 몇 분보다 오래된 요청은 거부하세요.
- 비밀 키를 안전하게 저장하세요. 환경 변수나 시크릿 관리자를 사용하고, 절대 하드코딩하지 마세요.
타임스탬프와 재전송 공격 처리
많은 제공자가 서명 헤더에 타임스탬프를 포함합니다. 예를 들어 헤더는 t=1614556800,v1=abc123...와 같을 수 있습니다. 다음을 수행해야 합니다.
- 타임스탬프와 서명을 추출합니다.
- 타임스탬프가 허용 가능한 범위(예: 5분) 내에 있는지 확인합니다.
- 타임스탬프와 페이로드를 사용하여 서명을 다시 계산합니다.
- 서명을 안전하게 비교합니다.
이렇게 하면 공격자가 오래된 유효한 요청을 재전송하는 것을 방지할 수 있습니다.
재시도: 실패를 우아하게 처리하기
웹훅은 공용 인터넷을 통해 전달되므로 실패는 불가피합니다. 엔드포인트가 다운되거나 느리거나 500 오류를 반환할 수 있습니다. 견고한 제공자는 실패한 전달을 재시도하지만, 여러분이 제공자라면 여러분 쪽에서도 재시도를 처리해야 합니다.
지터를 사용한 지수 백오프
재시도할 때 즉시 재시도로 엔드포인트를 공격하지 마세요. 지수 백오프를 사용하세요. 실패할 때마다 더 오래 기다립니다. 많은 웹훅이 동시에 실패할 때 몰려드는 문제를 피하려면 지터(무작위성)를 추가하세요.
일반적인 재시도 일정은 다음과 같습니다.
| 시도 | 지연(대략) |
|---|---|
| 1 | 즉시 |
| 2 | 30초 |
| 3 | 5분 |
| 4 | 30분 |
| 5 | 2시간 |
최대 시도 횟수 후에는 전달을 실패로 표시하고 경고합니다.
멱등성: 안전한 재시도의 핵심
재시도로 인해 중복 전달이 발생할 수 있으므로, 웹훅 핸들러는 멱등해야 합니다. 즉, 동일한 이벤트를 여러 번 처리해도 한 번 처리한 것과 동일한 효과를 가져야 합니다.
멱등성을 달성하는 방법:
- 페이로드에 제공된 고유 이벤트 ID(예:
id필드)를 사용하세요. 처리된 ID를 고유 제약 조건이 있는 데이터베이스에 저장하세요. - 작업을 멱등하게 설계하세요. 예를 들어 삽입 대신 upsert를 사용하거나 리소스를 생성하기 전에 이미 존재하는지 확인하세요.
- 작업이 무거우면 빠르게 2xx를 반환하고 비동기적으로 처리하세요. 이렇게 하면 재시도를 유발하는 타임아웃 가능성이 줄어듭니다.
제공자 측에서 재시도 구현하기
웹훅 시스템을 구축하는 경우 안정적인 전달 메커니즘이 필요합니다. 다음은 단순화된 접근 방식입니다.
- 이벤트가 발생하면 페이로드와 대상 URL로 전달 작업을 큐에 넣습니다.
- 워커가 작업을 가져와 타임아웃(예: 10초)과 함께 HTTP POST를 보냅니다.
- 응답 상태가 2xx가 아니면 지수 백오프로 재시도를 예약합니다.
- 시도 횟수를 추적하고 최대치(예: 5회) 후 포기합니다.
- 실패를 기록하고 사용자가 수동으로 재생할 수 있는 방법을 제공합니다.
영구 큐(예: Redis, RabbitMQ 또는 데이터베이스 테이블)를 사용하여 재시도가 재시작 후에도 유지되도록 하세요.
모두 통합하기: 웹훅 수신자 체크리스트
웹훅 수신자를 구축할 때 다음 체크리스트를 따르세요.
- 제공자의 비밀 키와 상수 시간 비교를 사용하여 서명을 검증하세요.
- 재전송 공격을 방지하기 위해 타임스탬프를 확인하세요.
- 빠르게 2xx로 응답하세요. 무거운 처리를 하기 전에 수신을 확인하세요.
- 필요한 경우 큐를 사용하여 비동기적으로 처리하세요.
- 이벤트 ID를 추적하여 멱등성을 보장하세요.
- 디버깅과 감사를 위해 모든 것을 기록하세요.
- 실패를 모니터링하고 반복적인 재시도에 대한 경고를 설정하세요.
FAQ
서명된 웹훅 페이로드란 무엇인가요?
서명된 페이로드는 공유 비밀 키로 생성된 암호화 서명(일반적으로 HMAC-SHA256)을 포함합니다. 수신자는 서명을 다시 계산하여 요청이 예상 발신자로부터 왔고 변조되지 않았는지 검증합니다.
웹훅에 재시도가 필요한 이유는 무엇인가요?
네트워크 문제, 서버 다운타임 또는 일시적 오류로 인해 웹훅 전달이 실패할 수 있습니다. 지수 백오프를 사용한 재시도는 수신자에게 과부하를 주지 않으면서 이벤트가 결국 전달되도록 보장합니다.
중복 웹훅 처리를 방지하려면 어떻게 해야 하나요?
멱등성을 사용하세요. 페이로드에 고유 이벤트 ID를 포함하고, 처리된 ID를 저장하고, 작업을 멱등하게 설계하세요(예: upsert). 이렇게 하면 웹훅이 여러 번 전달되더라도 효과는 동일합니다.
웹훅 페이로드를 검사하거나 디버깅해야 하는 경우 JSON Formatter를 사용하면 JSON 데이터를 빠르게 예쁘게 출력하고 검증할 수 있습니다.