서비스가 특정 이벤트가 발생했을 때 반응해야 하는 경우가 있습니다. 결제가 완료되었습니다. 고객 기록이 변경되었습니다. 리포지토리가 푸시되었습니다. API를 매 분마다 폴링하여 “새로운 항목이 있나요?”라는 질문을 반복하는 것보다, 이벤트가 발생했을 때 원본 시스템이 호출하는 것을 허용할 수 있습니다.
대부분의 웹 훅 예제 기사에서는 여기서 멈칫합니다. 그들은 경로를 보여주고 JSON 본문을 출력하고 반환하고, 그만합니다. 그 버전은 누군가가 위조된 요청을 보내거나 유효한 요청을 재생하거나, 핸들러가 파싱되기 전에 서명 확인이 이루어지기 전에 프레임워크가 본문을 파싱했을 때 핸들러가 깨지기까지 작동합니다. 200이 안내서는 실제 프로덕션에서 사용하는 경로를 따릅니다. 예제는 복사하기 쉬우면서도 중요한 부분을 포함합니다: raw body handling, HMAC verification, timestamp checks, fast acknowledgment, and practical debugging.
목차
웹 훅이란 무엇이며 왜 사용하는지
- __CAPGO_KEEP_0__
- Webhook HTTP 요청의 구조
- 웹 훅 서명이 안전하게 검증되는 방법
- 재생 공격에 대비하는 방법
- Node.js에서 Webhook 수신기 만들기
- Python에서 Webhook 수신기 만들기
- Go에서 Webhook 수신기 만들기
- 중요한 디버깅 기법
- 생산 환경에 적합한 웹후크 체크리스트
- 웹후크에 대한 자주 묻는 질문
웹훅이란 무엇이며 사용하는 이유는?
계정 제공업체는 02:13에 청구서를 지불한 것으로 표시합니다. 앱이 02:14에 알게 되면 고객은 즉시 접근할 수 있습니다. 앱이 다음 폴링 사이클에 알게 되면 고객은 기다리게 되고, 지원팀은 티켓을 받고, 로그는 불필요한 잡음으로 채워집니다. 웹훅은 이러한 시간 문제를 해결하기 위해 이벤트가 발생했을 때 HTTP 콜백을 전송하여 해결합니다.
실제로 웹훅은 시스템 간의 이벤트 기반 POST입니다. 제공업체는 변경 사항을 감지하고, 예를 들어 invoice.paid, order.created, 또는 push,을 감지하고, 이벤트 데이터를 URL을 제어하는 URL로 전송합니다. 이로써 폴링이 생성하는
실제 시스템에서 이 패턴이 나타난다. 이는 사업 이벤트에 매핑되기 때문입니다. 스티프는 결제 결과를 게시합니다. GitHub는 저장소 활동을 게시합니다. Shopify는 주문 업데이트 게시합니다. 형식은 간단하지만, 실제 운영 환경에서는 그렇지 않습니다. 금액, 접근, 또는 재고를 업데이트하는 웹훅은 API 공개 엔드포인트와 동일한 주의를 기울여야 합니다. 특히 재시도, 중복, 그리고 신뢰할 수 없는 트래픽이 들어오면 더욱 그렇습니다.
웹훅 흐름을 프레임하는 데 도움이 되는 정신 모델
웹훅 흐름을 프레임하는 데 도움이 되는 정신 모델
- 웹훅 흐름을 프레임하는 데 도움이 되는 정신 모델은 네 가지 요소가 함께 작동하는 것과 같습니다. . 이벤트를 감지하는 서비스.
- 목적지 엔드포인트. __CAPGO_KEEP_0__이 받는 HTTP 경로.
- 이벤트. 발생한 이름이 지정된 변경 사항, 예를 들어
invoice.paid또는push. - 리퀘스트 본문. code이 필요한 변경 사항의 세부 정보가 포함된 본문.
제공자가 이미 발생한 것을 대한 사실을 보내는 경우, __CAPGO_KEEP_0__의 임무는 발신자를 확인하고, 요청이 최신인지 확인하고, 변경 사항을 적용하는 것입니다. 마지막 부분은 많은 기본 튜토리얼이 인정하지 않는다.
실용적인 규칙: 이벤트 기반 업데이트에 웹후크를 사용하고, 예약된 읽기, 백필, 제공자가 외부 이벤트를 제공하지 않는 경우에는 폴링을 사용하십시오.
보다 광범위한 제품을 개발하는 팀 __CAPGO_KEEP_0__웹후크는 일반적으로 시스템을 불필요한 요청 트래픽 없이 동기화하는 이벤트 층이 됩니다. 통합-heavy 서비스를 개발하는 경우 Capgo 백엔드 개발 관련 글 production에서 성공하고 실패하는 것
production에서 잘 작동하는 설정은 보통 설계가 단순합니다. 필요한 이벤트만 구독하고, 제공자나 이벤트 종류에 따라 엔드포인트를 범위로 지정합니다. 이벤트 ID를 저장하여 중복 전달이 발생하지 않도록 하세요. 요청이 검증되고 큐에 넣어질 때 빠른 2xx 응답을 반환하고, 느린 비즈니스 로직은 비동기적으로 처리하세요.
fragile한 버전은 쉽게 식별할 수 있습니다. 하나의 일반 엔드포인트가 모든 것을 처리합니다. 서명 확인이 초기 테스트 중에 건너뛰어지고 다시 돌아오지 않습니다. 핸들러는 이벤트가 유효하거나陈舊인지 확인하기 전에 중요 테이블에 직접 쓰고, 서명 확인을 건너뛰어도 됩니다. 데모에서 작동하지만 재시도 폭탄, 제공자 장애, 또는 공격자가 이전 요청을 재생산하는 경우에 실패합니다.
이 가이드의 나머지 부분은 이 트레이드 오프로 정의됩니다. 웹후크 수신자 'hello world' 버전은 작습니다. production-ready 버전은 서명 확인, 재생 방어, 중복 처리, 디버깅 훅을 포함하여 시작부터 구현합니다.
웹후크 HTTP 요청의 구조
__CAPGO_KEEP_0__
code을 작성하기 전에, 요청을 프레임워크 객체 대신 raw HTTP로 보는 것이 도움이 됩니다. 일반적인 웹후크는 public endpoint로 HTTP POST를 보내며, 헤더와 JSON 형식의 바디를 포함합니다.
간단한 raw 요청
POST /webhooks/orders HTTP/1.1
Host: your-app.example
Content-Type: application/json
User-Agent: Provider-Webhooks/1.0
X-Webhook-Signature: sha256=abc123example
X-Webhook-Timestamp: 1712345678
{
"event": "order.created",
"id": "evt_123",
"data": {
"order_id": "ord_456",
"status": "created"
}
}
중요한 부분은 다음과 같습니다:
- Method. 실제로 웹후크 전달은 일반적으로 POST 요청입니다.
- Content-Type. 대부분의 현대 제공자는 JSON을 보내줍니다.
- User-Agent. 디버깅을 위해 도움이 되지만, 신뢰를 위한 것은 아닙니다.
- 서명 헤더. 제공자의 인증 확인을 포함합니다.
- 타임스탬프 헤더. 사용자 요청이 오래된 것인지 또는 재생된 것인지 거부하는 데 사용됩니다.
몸체의 형태는 왜 중요한가요
당신의 code는 보통 모든 field에 관심이 없습니다. 이벤트 타입, 이벤트 식별자, 그리고 내부의 business object에만 관심이 있습니다. data. 그 이유는 좋은 핸들러는 필요한 것만 파싱하고 나머지 로그를 디버깅을 위해 남겨두기 때문입니다.
OpenAPI는 이 패턴을 직접 모델링합니다. OpenAPI 3.1.0은 첫 번째급 웹훅 지원을 제공하며 top-level object를 사용하여 각 웹훅을 Path Item과 같은 방식으로 설명합니다. 제공자에 의해 트리거되는 웹훅입니다. canonical 예제는 웹훅에 operation, JSON 요청 본문, 그리고 수신 확인을 나타내는 response를 사용합니다. webhooks OpenAPI 웹훅 예제 newPet 만약 당신이 자신의 수신자 또는 제공자 계약을 문서화하고 있다면, 강력한 예제가 추상적인 스키마 문장보다 더 도움이 됩니다. 나는 SheetMergy의 __CAPGO_KEEP_0__ 문서 예제와 같은 참조를 사용하는 것을 좋아합니다. post Webhook 200 Receipt Operation.
JSON SheetMergy’s API doc examples 이러한 이유는 요청 예시, field 설명 및 기대 응답이 어떻게 연결되는지 명확하게 하기 때문입니다.
웹후크는 전송层에서 단순합니다. 대부분의 실패는 헤더, 바디 인코딩 또는 서명 규칙에 대한 일치하지 않는 가정으로부터 발생합니다.
웹후크 서명 인증을 안전하게 nasıl kullanırım?
서명된 웹후크는 한 가지 질문에 답합니다: 공유 비밀을 알고 있는 사람으로부터 이 페이로드가 왔는지 여부.
이것은 요청이 최근인지 여부 또는 이미 처리했는지 여부를 묻는 것과 다릅니다. 서명 인증은 첫 번째 게이트가 아니라 마지막 게이트입니다.

인증 흐름
일반적인 HMAC 흐름은 다음과 같습니다.
- 제공자의 헤더에서 서명을 읽습니다.
- 제공된 헤더에서 서명을 읽습니다. 원본 요청 바디를 정확하게 읽습니다. 원본 요청 바디를 정확하게 읽습니다.
- 보안 구성에서 웹후크 시크릿을 로드하세요.
- 같은 알고리즘을 사용하여 예상 HMAC을 재계산하세요.
- 타이밍 안전한 비교를 사용하여 받은 서명과 계산된 서명을 비교하세요.
- 일치하지 않으면 요청을 거부하세요.
JSON을 먼저 파싱하는 경우, 공백을 재정렬하거나 인코딩 세부 정보를 변경하는 경우, 해시를 계산할 때 잘못된 구현이 많습니다. 제공자의 서명과 일치하지 않습니다.
실제 code에서 무엇을 주의해야 하나요?
나는 가장 자주 보는 오류입니다:
- 가공되지 않은 JSON을 해싱하는 경우, 일치하지 않습니다.일치하지 않습니다.
JSON.stringify(req.body)정상적인 문자열 비교를 사용하지 마세요. 타이밍 안전한 비교를 사용하세요. - JSON을 먼저 파싱하는 경우, 공백을 재정렬하거나 인코딩 세부 정보를 변경하는 경우, 해시를 계산할 때 잘못된 구현이 많습니다. 제공자의 서명과 일치하지 않습니다.이러한 오류를 피하세요.
- 비밀번호를 하드코딩하지 마세요.. 환경 변수나 비밀번호 관리자에서 관리하세요.
- 헤더만 믿지 마세요.. 서명 헤더는 검증하지 않는 한 의미가 없습니다.
서비스 간 비밀번호 관리를 강화하는 팀에게는 Capgo의 API 앱 스토어 준수성을 위한 키 보안 가이드가 관련이 있습니다. 비밀번호 회전, 범위 내 접근, 로그에 누출되는 것을 피하는 것은 웹후크 수신자에게도 중요합니다.
일반적인 검증 예시
const crypto = require('crypto');
function verifySignature(rawBody, receivedSignature, secret) {
const expected = crypto
.createHmac('sha256', secret)
.update(rawBody)
.digest('hex');
const a = Buffer.from(receivedSignature, 'utf8');
const b = Buffer.from(expected, 'utf8');
if (a.length !== b.length) return false;
return crypto.timingSafeEqual(a, b);
}
이것은 의도적으로 일반적입니다. 실제 제공자들은 서명에 접두사를 붙인다, 시간을 조합하여 서명된 콘텐츠에 포함하거나, 해시를 다른 방식으로 인코딩합니다. 규칙은 동일합니다. 제공자의 정확한 서명 형식을 따르세요, 그리고 항상 원본 페이로드와 검증하세요.
재생 공격을 방지하는 5가지 중요 보안 조치의 체크리스트입니다.
재생 공격을 방지하기 위해 웹 애플리케이션에서 효과적으로 작동하는 5가지 중요 보안 조치의 체크리스트입니다.

__CAPGO_KEEP_0__ 질문에 답한다: 보낸 사람은 공유 비밀을 사용하여 이 페이로드를 생성했는가? 재생 보호는 다른 질문에 답한다: 지금 이 요청을 여전히 받아들여야 하는가? 실제 운영 환경에서는 두 가지 모두 필요하다.
__CAPGO_KEEP_0__에 대한 최소한의 확인이 실제로 중요하다
실제 재생 방어의 실용적인 시작은 signed timestamp와 관련된다. 제공자는 헤더나 signed 메시지에 timestamp를 포함하고, 수신자는 작은 허용 범위 내에 있는 요청만 거부한다.
__CAPGO_KEEP_0__의 흐름은 다음과 같이 나타나야 한다.
- __CAPGO_KEEP_0__를 제공자 정의된 위치에서 읽어라헤더 이름을 추측하지 마라.
- __CAPGO_KEEP_0__를 정수 또는 RFC 형식의 날짜로 파싱하라__CAPGO_KEEP_0__에 따라서.
- __CAPGO_KEEP_0__와 서버 클록을 비교하라.
- __CAPGO_KEEP_0__이 너무 오래된거나 너무 먼 미래의 요청을 거부하라.
- __CAPGO_KEEP_0__를 서명 체계의 일부로 확인하라 __CAPGO_KEEP_0__을 지원하는 제공자가 있다면.
그 마지막 점은 중요합니다. 만약 timestamp가 서명에 포함되지 않으면 공격자는 새로운 timestamp를 넣고 원래 body를 재생할 수 있습니다. 나는 항상 제공자의 정확한 서명 형식을 확인하기 전에 timestamp logic을 믿지 않습니다.
tolerance window를 선택하는 방법
5분은 일반적인 기본값입니다. 공격 창을 줄이기 위해 충분히 짧지만, 작은 시계 오차와 일반적인 네트워크 지연을 견딜 수 있습니다.
이것은 상호 보완적입니다. 30초 창은 더 안전해 보이지만, 실제 시스템에서 재시도, 큐잉, 또는 지역 지연이 포함된 경우 더 자주 실패합니다. 반면 30분 창은 운영하기 더 쉬우나, 서명된 요청이 노출된 경우 공격자에게 더 많은 시간을 제공합니다. 몇 분으로 시작하고 NTP와 서버를 동기화한 다음 제공자의 전달 패턴이 지원하는 경우에만 조정하세요.
재생 방어는 단순히 timestamp 검증만이 아닙니다.
timestamp 검증은陈舊된 요청을 차단합니다. 그러나 유효한 창 내에서 동일한 서명된 이벤트가 두 번 전달되는 경우에도 처리 중복을 막지 않습니다. 만약 동일한 서명된 이벤트가 유효한 창 내에서 두 번 전달된다면, 애플리케이션은 여전히 이를 식별해야 합니다.
두 번째 층을 사용하세요:
- 이벤트 ID 또는 전달 ID를 추적하세요. Redis와 같은 짧은 생명 주기의 저장소에서.
- 처리 핸들러를 idempotent로 처리하세요. 이러한 핸들러의 반복적인 전달이 중복된 주문, 이메일, 또는 계정 관리 액션을 생성하지 않도록 하세요.
- 기각된陈舊된 요청을 로깅하세요 비밀 키나 전체敏감 데이터를 로깅하지 않습니다.
- 빠른 응답을 반환합니다 유효성 검사와 큐 작업을 다른 곳에서 처리합니다.
유효 기간과 취소가 이미 생각하는 팀은 패턴을 인식합니다. Capgo의 Capgo 앱에 대한 토큰 취소 패턴의 안내서 Capacitor 앱에 대한 토큰 취소 패턴의 안내서 유효한 한 번의 자격 증명이나 요청은 영원히 신뢰되지 않아야 합니다.
서명된 것과陈舊한 것은 여전히 위험합니다.
Node.js에서 웹후크 수신기를 빌드하는 방법
Node.js와 Express는 여전히 심각한 수신기를 온라인으로 빠르게 구축하는 가장 빠른 방법입니다. 그러나 다른 것보다 더 중요한 한 가지 함정만 있으면 됩니다. Express가 객체로 변환하기 전에 raw body에 접근할 수 있어야 합니다.

실제 Express 예제
const express = require('express');
const crypto = require('crypto');
const app = express();
const PORT = process.env.PORT || 3000;
const WEBHOOK_SECRET = process.env.WEBHOOK_SECRET;
// Capture raw body for signature verification
app.use(
express.json({
verify: (req, res, buf) => {
req.rawBody = buf;
},
})
);
function safeEqual(a, b) {
const aBuf = Buffer.from(a, 'utf8');
const bBuf = Buffer.from(b, 'utf8');
if (aBuf.length !== bBuf.length) return false;
return crypto.timingSafeEqual(aBuf, bBuf);
}
function verifySignature(rawBody, secret, receivedSignature) {
const expected = crypto
.createHmac('sha256', secret)
.update(rawBody)
.digest('hex');
return safeEqual(expected, receivedSignature);
}
function isFresh(timestampHeader, toleranceSeconds = 300) {
const timestamp = Number(timestampHeader);
if (!Number.isFinite(timestamp)) return false;
const now = Math.floor(Date.now() / 1000);
return Math.abs(now - timestamp) <= toleranceSeconds;
}
app.post('/webhooks/example', async (req, res) => {
const signature = req.get('x-webhook-signature');
const timestamp = req.get('x-webhook-timestamp');
if (!WEBHOOK_SECRET) {
return res.status(500).send('Webhook secret is not configured');
}
if (!signature || !timestamp) {
return res.status(400).send('Missing required security headers');
}
if (!isFresh(timestamp)) {
return res.status(401).send('Stale webhook');
}
const valid = verifySignature(req.rawBody, WEBHOOK_SECRET, signature);
if (!valid) {
return res.status(401).send('Invalid signature');
}
// Acknowledge quickly
res.status(200).send('OK');
// Process after acknowledgement
try {
const event = req.body;
console.log('Accepted event:', event.event, event.id);
// enqueueJob(event)
} catch (err) {
console.error('Post-ack processing failed:', err);
}
});
app.listen(PORT, () => {
console.log(`Webhook receiver listening on ${PORT}`);
});
이 구조가 지속되는 이유
여러 선택지 중 일부는 의도적으로 선택된 것입니다.
- Raw body 캡처는 미들웨어에서 발생합니다.그것은 원래 바이트를 해싱하기 위해 보존합니다.
- 시간戳는 비즈니스 로직 전에 확인됩니다.기존 트래픽에 대한 작업을 할 필요가 없습니다.
- 경로가 반환됩니다.
200빠르게.길게 실행되는 작업은 큐 또는 배경 작업에 속해야 합니다. - 받은 후 처리는 분리됩니다.다운스트림 로직이 실패하더라도 수신자 경로는 작습니다.
비밀은 많은 웹후크 구현의 약점입니다. 소스에 넣지 마세요, 테스트 fixture에 붙여넣지 마세요, 로그에 반영하지 마세요. rotation 및 CI 처리에 대한 더 광범위한 프로세스가 필요하다면 Capgo의 CI/CD pipeline에서 비밀 관리에 대한 지침을 참조하세요. CI/CD pipeline에서 비밀 관리에 대한 지침 운영 측면을 잘 다룹니다.
간단한_walkthrough를 통해 움직이는 조각을 실제로 볼 수 있습니다.
실제 시스템에서 변경하고 싶은 점
실제 제공자 통합을위한 live 시스템에서는 이벤트 ID 중복 제거, 요청 ID와 함께 구조화된 로그, 확인 경로 뒤에 큐를 추가하는 것이 좋습니다. 또한 여러 제공자가 다중 서명 형식 사용하는 경우 단일 일반 엔드포인트를 피하고, 분리된 핸들러를 사용하는 것이 더 쉽게 이해하고 깨지기 어려운 것입니다.
Python에서 Webhook 수신기 만들기
Flask는 명확한 웹 훅 예제를 위해 깨끗한 선택입니다. 요청 처리는 명확하고, Python의 표준 라이브러리는 HMAC를 위해 이미 필요한 것을 제공합니다.
Node와 동일한 점을 기억해야 합니다. raw request bytes와 비교하여 parsed JSON dict를 확인하세요.
서명 및 타임스탬프 확인을 포함한 Flask 예제
import os
import time
import hmac
import hashlib
from flask import Flask, request, jsonify
app = Flask(__name__)
WEBHOOK_SECRET = os.environ.get("WEBHOOK_SECRET", "")
def is_fresh(timestamp_header, tolerance_seconds=300):
try:
timestamp = int(timestamp_header)
except (TypeError, ValueError):
return False
now = int(time.time())
return abs(now - timestamp) <= tolerance_seconds
def verify_signature(raw_body, secret, received_signature):
expected = hmac.new(
secret.encode("utf-8"),
raw_body,
hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, received_signature)
@app.route("/webhooks/example", methods=["POST"])
def webhook():
if not WEBHOOK_SECRET:
return "Webhook secret is not configured", 500
signature = request.headers.get("X-Webhook-Signature")
timestamp = request.headers.get("X-Webhook-Timestamp")
if not signature or not timestamp:
return "Missing required security headers", 400
if not is_fresh(timestamp):
return "Stale webhook", 401
raw_body = request.get_data()
if not verify_signature(raw_body, WEBHOOK_SECRET, signature):
return "Invalid signature", 401
payload = request.get_json(silent=True) or {}
# Acknowledge receipt
response = jsonify({"status": "ok"})
# In production, queue payload here instead of heavy sync work
print("Accepted event:", payload.get("event"), payload.get("id"))
return response, 200
if __name__ == "__main__":
app.run(port=5000, debug=True)
Flask에 특이한 세부 사항
request.get_data() 이것이 가장 중요한 호출입니다. 요청 본문의 raw bytes를 제공합니다. 바로 request.json로 이동하면 서명 불일치가 혼란스러워집니다.
몇 가지 implementation notes:
- 이 대신 평등 연산자 대신 사용하세요.
hmac.compare_digest헤더가 누락된 경우 클라이언트 오류로 처리하고 조기 거부하세요. - JSON 파싱을 위해 사용하세요. Flask가 오류를 발생시키기 보다는 오류 처리를 제어하고 싶다면. 경로를 얇게 유지하세요. .
- 리퀘스트가 비용이 많이 드는 작업을 트리거하면 작업을 큐에 넣으세요.
silent=True서명 불일치로 인한 디버깅은 보안 검증을 완화하는 것이 아닙니다. 서명 불일치로 인한 디버깅은 정확히 해시된 바이트를 출력하고 제공자가 기대하는 서명 형식을 출력하세요. 팀이 일반적으로 막히는 곳은 - 일반적인 실패 경로는 JSON 본체를 수동으로 빌드하여 테스트한 다음 실제 제공자와switching하고 서명이 더 이상 일치하지 않으면서 서명이 더 이상 일치하지 않습니다. 일반적으로 이는 다음 중 하나의 경우입니다: 제공자가 시간이 지남에 따라 봉투를 서명하거나 서명이 제공자가 가정한 것과 다르게 인코딩되거나 미들웨어가 본체를 변경하기 전에 검증을 변경했습니다.Use
instead of plain equality.
Treat missing headers as a client failure
and reject early.
When that happens, stop changing the crypto code at random. Capture the raw headers and raw body, reproduce the hash in a tiny isolated script, and only then put it back into the Flask route.
웹훅 수신기를 Go로 구축하는 방법
Go는 웹훅 수신기에 적합한 언어입니다. 표준 라이브러리가 충분하기 때문입니다. 프레임워크가 필요하지 않으며 code를 신뢰할 수 있습니다.
웹훅 수신에서 주의할 점은 요청 본문 처리입니다. r.Body 요청 본문은 스트림입니다. 한 번 읽고, 읽은 바이트를 해시하고, 그 해시된 바이트에서 언마샬링합니다.
표준 라이브러리 예제
package main
import (
"crypto/hmac"
"crypto/sha256"
"crypto/subtle"
"encoding/hex"
"encoding/json"
"io"
"log"
"net/http"
"os"
"strconv"
"time"
)
type WebhookPayload struct {
Event string `json:"event"`
ID string `json:"id"`
Data json.RawMessage `json:"data"`
}
func isFresh(timestampHeader string, toleranceSeconds int64) bool {
ts, err := strconv.ParseInt(timestampHeader, 10, 64)
if err != nil {
return false
}
now := time.Now().Unix()
diff := now - ts
if diff < 0 {
diff = -diff
}
return diff <= toleranceSeconds
}
func verifySignature(rawBody []byte, secret string, received string) bool {
mac := hmac.New(sha256.New, []byte(secret))
mac.Write(rawBody)
expected := hex.EncodeToString(mac.Sum(nil))
if len(expected) != len(received) {
return false
}
return subtle.ConstantTimeCompare([]byte(expected), []byte(received)) == 1
}
func webhookHandler(secret string) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
signature := r.Header.Get("X-Webhook-Signature")
timestamp := r.Header.Get("X-Webhook-Timestamp")
if signature == "" || timestamp == "" {
http.Error(w, "missing required security headers", http.StatusBadRequest)
return
}
if !isFresh(timestamp, 300) {
http.Error(w, "stale webhook", http.StatusUnauthorized)
return
}
rawBody, err := io.ReadAll(r.Body)
if err != nil {
http.Error(w, "failed to read body", http.StatusBadRequest)
return
}
if !verifySignature(rawBody, secret, signature) {
http.Error(w, "invalid signature", http.StatusUnauthorized)
return
}
var payload WebhookPayload
if err := json.Unmarshal(rawBody, &payload); err != nil {
http.Error(w, "invalid json", http.StatusBadRequest)
return
}
w.WriteHeader(http.StatusOK)
w.Write([]byte("OK"))
log.Printf("accepted event=%s id=%s", payload.Event, payload.ID)
}
}
func main() {
secret := os.Getenv("WEBHOOK_SECRET")
if secret == "" {
log.Fatal("WEBHOOK_SECRET is not set")
}
http.HandleFunc("/webhooks/example", webhookHandler(secret))
log.Println("listening on :8080")
log.Fatal(http.ListenAndServe(":8080", nil))
}
Go가 웹훅 수신에서 안정적인 언어인 이유
다음과 같은 이점이 있습니다.
- 핸들러는 명확합니다.hidden middleware magic이 없습니다.
- 타입이 도움이 됩니다.edge에서 타입이 도움이 됩니다. 헤더 파싱, 타임스탬프 변환, JSON 디코딩 모두 명확하게 실패합니다.
- The standard crypto packages are enough. No extra dependency for basic HMAC verification.
운영 참고 사항
웹훅 볼륨이 증가하면 Go의 동시성 모델은 HTTP 진입점을 변경하지 않고 배경 작업을 분산할 수 있는 여유를 제공합니다. 그 때도 수신자 너비를 유지하세요. 수락, 유효성 검사, 확인, 그리고 응답이 돌아오기 전에 전달하세요.
Go 웹훅 핸들러 중 가장 강력한 것들은 평범합니다. Transport 인증과 비즈니스 로직을 섞지 않으며, 응답이 돌아오기 전에 데이터베이스 작업을 수행하지 않습니다.
중요한 디버깅 기법
웹훅 버그는 일반적으로 스택 트레이스 대신 지원 메시지로 나타납니다. 제공자는 이벤트를 전달했다고 말합니다. 엔드포인트는 앱에 도달한 것이 없다고 말하거나, 요청이 처음으로 유효하게 보이지만 서명 검증이 실패했다고 말합니다. 그 때 디버깅은 정확한 HTTP 교환을 재구성하고, 바이트 단위로 증명하는 것입니다. 어디서 깨졌는지.

실용적인 디버깅 도구
시작은 wire format입니다.
서명 검증이 실패하면, 원본 요청 본체와 검증에 사용된 헤더를 정확하게 캡처하세요. 실제로 버그는 평범합니다. 프레임워크가 JSON을 해싱하기 전에 파싱했거나, 프록시가 인코딩을 변경했거나, 테스트 재생이 원래 타임스탬프 헤더를 놓쳤을 수 있습니다. 파싱된 객체를 로깅하는 것은 충분하지 않습니다. 원본 바이트와 검증 입력이 필요합니다.
이 도구들은 문제를 빠르게 분리합니다:
- 원본 요청 캡처. 수사 중에 로그 헤더, 콘텐츠 타입, 콘텐츠 길이, 그리고 수정되지 않은 본문을 캡처합니다.
- 요청 검사 엔드포인트. 제공자와 협력하면서 로컬 수신자에 테스트할 수 있도록 하는 서비스들,
webhook.site원본 송신자가 전송한 것을 확인하는 데 도움이 됩니다. - 로컬 터널링.
ngrok및 같은 도구들은 제공자와 협력하면서 로컬 수신자에 테스트할 수 있도록 합니다. - 수동 재생. __CAPGO_KEEP_0__ 또는 제공자 페이로드가 문제인지 확인하기 위해 동일한 본문과 헤더를 사용하여 Postman 또는 같은 도구로 요청을 재구성합니다.
curlor Postman using the same body and headers. That is the quickest way to confirm whether your code or the provider payload is the issue. - . 제공자 대시보드에는 응답 코드, 재시도 기록, 요청 식별자가 포함되어 있습니다. 이 로그를 사용하여 제공자 로그와 일치시킬 수 있습니다.Provider delivery logs. The sender dashboard often includes response codes, retry history, and request identifiers you can match against your logs.
패턴은 중요합니다. 외부에서 내부로 작업하세요. 먼저 제공자가 보낸 것을 확인하세요. 그 다음 서버가 받은 바이트가 동일한지 확인하세요. 마지막으로 code이 동일한 바이트와 동일한 비밀키와 타임스탬프 규칙으로 동일한 해시를 생성했는지 확인하세요.
실제로 도움이 되는 로깅
좋은 웹후크 로그는 한 번의 검색으로 세 가지 질문에 답해야 합니다.
| 질문 | 유용한 로그 필드 |
|---|---|
| 요청이 도착했는가? | route, method, received_at |
| 거부된 이유는 무엇인가? | missing_header, stale_timestamp, signature_failed |
| 후속 조치가 가능할까? | event_id, provider_request_id |
실제 시스템에서는 네 번째 필드가 도움이 됩니다. 지역 필드를 추가하세요. request_id 받은 요청을 따라서 앱, 큐, 및 워커 로그를 통해 요청을 추적할 수 있도록 수신자에 의해 생성됩니다.
저장할 때 선택적이세요. 비밀을 로깅하지 마세요. 고객 데이터, 접근 토큰, 또는 청구 세부 정보가 포함된 전체 생산 페이로드를 덤프하지 마세요. 고객 데이터, 접근 토큰, 또는 청구 세부 정보가 포함된 전체 생산 페이로드를 덤프하지 마세요. 대신 메타데이터와 짧은 본문 해시를 로깅하세요. 그럼에도 불구하고 재시도와 두 번의 전달이 동일한지 확인할 수 있습니다.
원래 입력과 함께 실패를 재현하세요.
기본 튜토리얼이 생략하는 부분입니다. 실패하는 요청을 정확히 재생할 수 없다면 추측하고 있습니다.
실패하는 웹후크를 저장하세요:
- 원본 본문 바이트
- 모든 서명 관련 헤더
- 요청 시간
- 콘텐츠 유형
- 제공자 요청 ID
그런 다음 스테이징 엔드포인트에 다시 재생하세요. 재생이 통과하면 전송 중에 무엇이 변경되었는지 비교하세요. 일반적인 원인에는 요청 본문을 정규화하는 미들웨어, 문자 인코딩 불일치, 헤더를 제거하거나 다시 쓰는 로드 밸런서가 포함됩니다. 또한 팀이 실제 요청 본문을 대신하여 예쁘게 인쇄된 대시보드 뷰에서 페이로드를 복사하는 경우도 실패를 유발합니다. whitespace 차이만으로 HMAC 검증을 깨뜨릴 수 있었습니다.
더 광범위한 릴리스 및 모바일 전송 문제 해결에 대한 Capgo의 가이드를 참조하세요. Capacitor의 OTA 업데이트 디버깅 도구. Different transport, same lesson. Capture the actual request path before changing application code.
서명 검증이 실패하면, 원시 바이트, 검증 시 사용된 정확한 헤더 및 타임스탬프 값을 확인하고 암호화 code를 조작하지 마십시오.
운영 환경에 적합한 웹후크 목록
웹후크 처리기는 스테이징 환경에서 정상적으로 작동하는 것처럼 보일 때까지, 2시의 첫 번째 재시도 폭탄, 잘못된 페이로드, 서명 불일치와 같은 문제가 발생할 때까지. 운영 환경의 바는 더 높습니다. 수신자는 위조된 요청을 거부하고, 유효한 재시도를 수락하고, 실패를 디버깅하는 데 필요한 신호를 제공하는 동시에敏感한 데이터를 노출하지 않도록 해야 합니다.
보안 및 정확성 검사
- 모든 요청 서명을 검증하십시오.엔드포인트 URL이 누설됩니다. 테스트 URL이 채팅에서 공유됩니다. 서명 검증은 공유된 비밀 키를 알고 있는 송신자가 알려주는 제어입니다.
- 기존 요청을 거부하십시오.유효한 서명이 있는 기존 페이로드는 여전히 재생할 수 있습니다. 제공자의 재시도 모델과 일치하는 타임스탬프 허용 범위를 적용하십시오.
- 원본 바디를 해시하십시오, parsed JSON이 아닙니다.미들웨어는 키를 재배치, 공백을 정규화하거나 인코딩을 변경할 수 있습니다. 검증은 실제로 도착한 정확한 바이트에 대해 실행되어야 합니다.
- 비밀 키를 code 에서 분리하세요.. 환경 변수는 기본입니다. 자격 증명이 정기적으로 회전되거나 여러 환경에서 실행되는 경우 비밀 키 관리자가 더 적합합니다.
- 인증 오류 시 폐쇄. 서명 헤더가 누락되거나 오류가 발생하거나 예상하지 못한 스키마를 사용하는 경우 요청을 거부하고 이유를 로깅하세요.
신뢰도 검사
- 빠른 인식. 제공자는 일반적으로 2xx를 성공으로 처리하므로 요청을 검증하고 필요한 것을 저장한 후 작업을 큐 또는 워커로 이동하세요.
- 핸들러를 무결성. 동일한 이벤트가 여러 번 도착할 수 있으므로 이벤트 ID, 전달 ID 또는 안정적인 제공자 식별자 중 하나를 사용하여 이벤트의 주요 효과를 분리하세요.
- 예측 가능한 오류 코드 반환. 잘못된 입력에 대해 사용하세요.
400for401또는403인증이 실패한 경우, 그리고5xx만약 시스템이 문제가 된다면. 이로 인해 제공자 재시도 동작을 더 쉽게 이해할 수 있습니다. - 파싱하기 전에 제한을 설정하세요Cap 요청 크기, 콘텐츠 유형 및 헤더 수를 조기 설정하세요. 이로 인해 웹후크 엔드포인트가 일반적인 인식 구멍으로 변하지 않습니다.
- 계약을 좁게 유지하세요지원하는 field 및 이벤트 유형만 수락하세요. 느슨한 파싱은 처음에는 편리하게 느껴지지만 제공자 API 변경 시 비용이 많이 들 수 있습니다.
관찰 가능성 확인
좋은 웹후크 작업은 흥미롭지 않습니다. 팀은 빠르게 세 가지 질문에 답할 수 있습니다: 받았나요? 인증되나요? 하류 처리가 성공했나요?
그 표준을 사용하세요:
- 받은 것을 추적하세요, 인증을 확인하세요, 처리를 추적하세요.
- 요청 ID, 이벤트 ID, 서명 상태 및 타임스탬프 왜곡을 로깅하세요.
- __CAPGO_KEEP_0__의 지연 시간, 처리 속도 및 재시도 양을 측정합니다..
- __CAPGO_KEEP_0__의 안전한 재생 경로를 스테이징 또는 재배포 워크플로우에 유지합니다..
- 패턴 변경에 대한 경고를 설정합니다.예를 들어, 서명 실패의 급증 또는 중복 배달과 같은 경우입니다.
Capgo은 업데이트 워크플로우의 보다 광범위한 운영점을 보여주는 유용한 예입니다. 그것은 배포와 관찰성에 대한 도구를 포함하고 있으며, 그것의 생태계의 일부도 웹후크 관련된 흐름에 접촉합니다. 이 교훈은 실제입니다. 배달 시스템은 수신부터 완료까지의 시각성을 필요로합니다.
웹후크 수신자는 일반적으로 프로덕션에 대해 좋은 상태를 유지합니다. 만약 어떤 항목이 누락된다면, 그 결함은 데모 중에 나타나지 않고 사고 중에 나타납니다.
웹후크에 대한 자주 묻는 질문
code의 상태를 반환해야 하는 것은 무엇인가요?
웹후크를 수락했을 때는 2xx 를 반환하세요. 유효성 검사 실패 시, 반환하는 클라이언트 또는 인증 오류를 실패와 일치시켜야 합니다. 예를 들어, 400 malformed input 또는 401 __CAPGO_KEEP_0__
유효하지 않은 인증 데이터를 위한 로직을 유지하여 제공자 대시보드가 더 쉽게 해석되도록 하세요.
웹훅을 동기적으로 처리해야 하나요?
보통 그렇지 않습니다. 유효성을 검사하고 승인한 다음 실제 작업을 큐 또는 백그라운드 작업으로 밀어내세요. 그럼 전달 경로가 빠르고 느린 하위 스트림 처리로 인한 중복 재시도가 줄어듭니다.
재시도를 어떻게 처리해야 하나요?
재시도가 발생할 것이라고 가정하세요. 핸들러에 중복성을 구축하여 동일한 이벤트를 받았을 때 중복된 부작용이 발생하지 않도록 하세요. 이벤트 ID 또는 제공자 전달 ID가 일반적으로 중복성을 구축하는 데 사용되는 anchor입니다.
이벤트가 순서대로 도착하지 않을 경우 어떻게 하나요?
가능한 경우 핸들러를 순서에 대한 내성을 설계하세요. 만약 비즈니스 프로세스가 순서가 필요하다면 스테이트를 저장하여 오래된 전환을 감지할 수 있도록 하세요. 전달 순서가 이벤트 순서를 반영하는 것은 가정하지 마세요.
웹훅 버전 변경에 어떻게 대응해야 하나요?
If your team ships Capacitor or Electron apps, Capgo 또는 Electron 앱을 배포하는 팀이 있다면 __CAPGO_KEEP_0__