웹훅 완벽 가이드: 서명된 페이로드와 재시도
웹훅에 서명과 재시도가 필요한 이유
결제 제공업체나 SaaS 플랫폼으로부터 웹훅을 수신할 엔드포인트를 구축했습니다. 테스트에서는 모든 것이 작동하지만, 프로덕션에서는 두 가지 문제에 직면합니다: 요청이 실제로 제공업체에서 온 것인지 어떻게 알 수 있으며, 서버가 다운되거나 느릴 때 어떻게 해야 할까요? 서명된 페이로드가 없으면 누구나 요청을 위조할 수 있습니다. 재시도가 없으면 단 한 번의 네트워크 장애로 이벤트가 영원히 유실될 수 있습니다.
이 가이드에서는 두 가지를 올바르게 구현하는 방법을 설명합니다. HMAC 서명, 검증 단계, 재시도 전략, 멱등성—안정적인 웹훅 처리의 세 가지 기둥을 다룹니다.
웹훅 서명의 작동 방식
웹훅 서명은 공유 비밀을 알고 있는 사람이 페이로드를 생성했음을 증명하는 암호화 해시입니다. 대부분의 제공업체는 HMAC-SHA256을 사용합니다. 발신자는 원시 요청 본문(때로는 타임스탬프 포함)에 대해 서명을 계산하여 헤더에 포함합니다. 수신자는 이를 재계산하여 비교합니다.
일반적인 흐름은 다음과 같습니다:
- 제공업체와 소비자가 비밀 키를 공유합니다(제공업체 대시보드에서 구성).
- 제공업체가 페이로드를 생성하고
HMAC-SHA256(secret, payload)를 계산하여X-Signature와 같은 헤더로 전송합니다. - 소비자가 요청을 수신하고 원시 본문을 읽어 HMAC을 재계산한 후 상수 시간 함수를 사용하여 비교합니다.
- 일치하면 이벤트를 처리하고, 그렇지 않으면 401로 거부합니다.
중요: 항상 수신된 원시 요청 본문을 사용해야 하며, 파싱 후 재직렬화한 버전을 사용해서는 안 됩니다. JSON 직렬화는 공백과 키 순서를 변경하여 서명을 깨뜨릴 수 있습니다.
재전송 공격을 방지하기 위한 타임스탬프 추가
서명된 페이로드만으로는 유효한 요청을 캡처한 공격자가 재전송할 수 있습니다. 이를 완화하려면 서명된 데이터에 타임스탬프를 포함하고 몇 분 이상 지난 요청은 거부하세요. 예를 들어:
signed_payload = timestamp + "." + raw_body
signature = HMAC-SHA256(secret, signed_payload)
소비자는 타임스탬프가 허용 가능한 범위(예: 5분) 내에 있는지 확인하고 서명이 일치하는지 검사합니다.
서명 검증 구현
Flask를 사용한 Python의 구체적인 예제를 살펴보겠습니다. X-Signature 헤더로 전송된 서명을 검증합니다.
import hmac
import hashlib
from flask import Flask, request, abort
app = Flask(__name__)
SECRET = b'your_shared_secret'
@app.route('/webhook', methods=['POST'])
def webhook():
signature = request.headers.get('X-Signature')
if not signature:
abort(401)
raw_body = request.get_data()
expected = hmac.new(SECRET, raw_body, hashlib.sha256).hexdigest()
if not hmac.compare_digest(expected, signature):
abort(401)
# 이벤트 처리
event = request.json
# ... 이벤트 처리 ...
return '', 200
핵심 사항:
- 타이밍 공격을 피하려면 상수 시간 비교를 위해
hmac.compare_digest(또는 동등한 함수)를 사용하세요. - 파싱 전에
request.get_data()를 통해 원시 본문에 접근하세요. - 빠르게 200을 반환하고, 무거운 처리는 비동기적으로 수행하세요.
재시도 전략 설계
서명이 있더라도 웹훅은 실패할 수 있습니다: 서버가 다운되거나, 500을 반환하거나, 시간 초과될 수 있습니다. 제공업체는 일반적으로 지수 백오프를 사용하여 재시도합니다. 소비자로서 재시도를 원활하게 처리해야 합니다.
고려해야 할 사항:
- 멱등성: 각 이벤트에는 고유 ID가 있어야 합니다. 재시도 시 중복 처리를 피하기 위해 처리된 ID를 저장하세요.
- 빠른 응답: 몇 초 이내에 2xx 상태로 수신 확인을 합니다. 이벤트를 백그라운드 처리를 위해 큐에 넣으세요.
- 자체 재시도 로직: 다운스트림 서비스를 호출하는 경우 백오프 및 회로 차단기를 사용한 재시도를 구현하세요.
대부분의 제공업체는 비2xx 응답이나 시간 초과 시 재시도하며, 종종 간격을 늘리면서 최대 24시간 동안 재시도합니다. 구체적인 내용은 제공업체 문서를 확인하세요.
예제: 멱등 이벤트 핸들러
데이터베이스 테이블을 사용하여 처리된 이벤트 ID를 추적합니다. 다음은 단순화된 패턴입니다:
def handle_event(event_id, payload):
if db.exists('processed_events', event_id):
return # 이미 처리됨
# 페이로드 처리
db.insert('processed_events', event_id)
# ... 실제 작업 ...
높은 처리량을 위해서는 이벤트 ID에 고유 제약 조건을 사용하고 중복 키 오류를 잡으세요.
비교: 서명된 웹훅 vs 서명되지 않은 웹훅
| 측면 | 서명된 페이로드 | 서명되지 않은 페이로드 |
|---|---|---|
| 보안 | 발신자 진위 확인 | 누구나 요청 위조 가능 |
| 재전송 방지 | 타임스탬프로 가능 | 없음 |
| 구현 노력 | 보통 (HMAC + 타임스탬프 확인) | 낮음 |
| 권장 대상 | 모든 프로덕션 웹훅 | 내부 테스트 전용 |
흔한 실수와 모범 사례
- 검증 전에 파싱하지 마세요: 항상 원시 본문에 대해 먼저 서명을 검증하세요.
- HTTPS 사용: 서명은 무결성을 보호하지만, HTTPS는 기밀성을 보호합니다.
- 비밀 교체: 다운타임을 피하기 위해 교체 중에 여러 활성 비밀을 지원하세요.
- 실패 로깅: 디버깅을 위해 서명 불일치 및 재시도를 기록하세요.
- 실제 페이로드로 테스트: webhook.site 또는 제공업체의 CLI와 같은 도구를 사용하여 이벤트를 시뮬레이션하세요.
FAQ
서명된 웹훅 페이로드란 무엇인가요?
서명된 페이로드에는 수신자가 요청이 신뢰할 수 있는 출처에서 왔고 변조되지 않았음을 확인할 수 있는 암호화 서명(보통 HMAC)이 포함됩니다.
작업을 중복하지 않고 웹훅 재시도를 처리하려면 어떻게 해야 하나요?
이벤트 ID를 추적하여 핸들러를 멱등적으로 만드세요. 처리하기 전에 ID가 이미 처리되었는지 확인하고, 그렇다면 작업을 반복하지 않고 성공을 반환하세요.
서명 없이 웹훅을 사용할 수 있나요?
프로덕션에서는 권장되지 않습니다. 서명이 없으면 합법적인 요청과 위조된 요청을 구분할 수 없습니다. 가능하면 항상 서명을 검증하세요.
웹훅 페이로드를 디버깅하거나 서버 로그를 분석할 때 JSON Formatter를 사용하면 JSON 구조를 빠르게 검사하고 검증할 수 있습니다.