Skip to Content
개발자웹훅시그니처 검증

시그니처 검증

웹훅 요청이 정말 페이리에서 온 건지 확인하려면 서명을 검증해야 해요.
페이리는 Standard Webhooks  규격으로 서명해요.
webhook-id·webhook-timestamp·원본 본문을 이어 붙인 문자열을 시크릿으로 HMAC-SHA256 서명해 webhook-signature 헤더에 담아 보내요.
수신 측은 같은 방식으로 다시 계산해 일치하는지 봐요.

검증 흐름

서명 규칙

항목
알고리즘HMAC-SHA256
입력 메시지`${webhook-id}.${webhook-timestamp}.${원본 본문}` (점으로 연결)
서명 키시크릿(whsec_…)에서 whsec_ 접두어를 떼고 base64 디코드한 바이트
출력base64 문자열. webhook-signature 헤더에 v1,<서명> 형태로 담겨요

반드시 원본 본문으로 검증하세요.
JSON을 파싱했다가 다시 직렬화하면 공백·키 순서가 달라져 서명이 어긋나요.
프레임워크의 raw body 옵션으로 원본 바이트를 보관한 뒤 검증하세요.

예시 (Node.js / Express)

import express from 'express' import crypto from 'node:crypto' const app = express() // whsec_ 접두어를 떼고 base64 디코드한 바이트가 서명 키 const SECRET = process.env.PAYRI_WEBHOOK_SECRET // whsec_... const secretBytes = Buffer.from(SECRET.replace(/^whsec_/, ''), 'base64') // 원본 본문 보관 (express.json의 verify 훅) app.use(express.json({ verify: (req, _res, buf) => { req.rawBody = buf } })) app.post('/webhook', (req, res) => { const id = req.get('webhook-id') ?? '' const timestamp = req.get('webhook-timestamp') ?? '' const signedContent = `${id}.${timestamp}.${req.rawBody}` const expected = crypto .createHmac('sha256', secretBytes) .update(signedContent) .digest('base64') // 헤더는 "v1,<서명>" 형태 — 버전 접두어를 떼고 비교 const received = (req.get('webhook-signature') ?? '').replace(/^v1,/, '') const ok = expected.length === received.length && crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(received)) if (!ok) return res.sendStatus(400) // 폐기 — 200 주지 않음 const { type, data } = req.body // TODO: webhook-id로 멱등 확인 후 처리 res.sendStatus(200) })

타이밍 공격을 피하려고 단순 === 대신 timingSafeEqual 같은 상수 시간 비교를 쓰는 걸 권장해요.
webhook-timestamp최초 발송 시각이고 재전송에도 그대로 유지돼요.
재시도는 최초 발송 후 최대 81분까지 이어질 수 있으니, 타임스탬프가 오래됐다고 요청을 거부하면 안 돼요.

다음 단계

재전송과 멱등성

검증 통과 후 중복 처리를 막아요.

SDK로 검증하기

직접 구현 대신 공식 SDK의 검증 유틸을 써요.

Last updated on