시그니처 검증
웹훅 요청이 정말 페이리에서 온 건지 확인하려면 서명을 검증해야 해요.
페이리는 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분까지 이어질 수 있으니, 타임스탬프가 오래됐다고 요청을 거부하면 안 돼요.
다음 단계
Last updated on