보안 검증
수신한 Webhook 요청이 실제로 apiacc에서 전송되었는지 서명을 검증하는 방법을 설명합니다.
모든 Webhook 요청에는 Apiacc-Timestamp, Apiacc-Event-Id, Apiacc-Signature 헤더가 포함됩니다. Apiacc-Signature 값은 v1=서명 형식이며, Webhook 등록 시 발급된 webhook_secret으로 요청 본문을 HMAC-SHA256으로 서명한 값입니다.
검증 절차
{timestamp}.{event_id}.{raw_body} 형태의 바이트열을 webhook_secret으로 HMAC-SHA256 서명한 뒤, 헤더의 Apiacc-Signature값과 상수 시간 비교로 일치 여부를 확인합니다.
Node.js
import crypto from "node:crypto"
function headerValue(value: string | string[] | undefined) {
return Array.isArray(value) ? value[0] : value ?? ""
}
function verifySignature(rawBody: Buffer, headers: Record<string, string | string[] | undefined>, secret: string) {
const timestamp = headerValue(headers["apiacc-timestamp"])
const eventId = headerValue(headers["apiacc-event-id"])
const signature = headerValue(headers["apiacc-signature"])
if (!timestamp || !eventId || !signature.startsWith("v1=")) {
return false
}
const signedPayload = Buffer.concat([Buffer.from(timestamp + "." + eventId + "."), rawBody])
const expected = Buffer.from(
"v1=" + crypto.createHmac("sha256", secret).update(signedPayload).digest("hex"),
)
const actual = Buffer.from(signature)
const age = Math.abs(Date.now() / 1000 - Number(timestamp))
if (age > 300 || expected.length !== actual.length) {
return false
}
return crypto.timingSafeEqual(expected, actual)
}Python 예시
Python
import hashlib
import hmac
import time
def verify_signature(raw_body: bytes, timestamp: str, event_id: str, signature: str, secret: str) -> bool:
if not timestamp or not event_id or not signature.startswith("v1="):
return False
signed_payload = timestamp.encode() + b"." + event_id.encode() + b"." + raw_body
expected = "v1=" + hmac.new(secret.encode(), signed_payload, hashlib.sha256).hexdigest()
if abs(time.time() - int(timestamp)) > 300:
return False
return hmac.compare_digest(expected, signature)raw body를 사용하세요
서명 검증에는 JSON.parse로 다시 직렬화한 본문이 아니라, 요청에서 받은 원본(raw) 바이트를 그대로 사용해야 합니다. 프레임워크의 body parser가 자동으로 파싱하는 경우 raw body 미들웨어를 별도로 설정하세요.