lifecarelog
프론트엔드

웹훅 안전하게 받기: HMAC 서명검증·재전송 방지·멱등 처리 3종 세트

Next.js Route Handler로 웹훅을 받을 때 필요한 HMAC 서명검증, 타임스탬프 재전송 방지, 멱등 키 처리를 프론트엔드 개발자 관점으로 정리했어요.

5분 읽기

결제 완료나 알림 발송 같은 이벤트를 외부 서비스가 우리 쪽으로 쏴 주는 게 웹훅이에요. 예전엔 백엔드 일이었는데, Next.js로 풀스택을 하다 보니 프론트엔드 개발자가 Route Handler로 직접 받는 경우가 많아졌어요. 혹시 웹훅 URL을 검증 없이 열어둔 적 없나요?

웹훅 엔드포인트는 공개된 문이에요. 주소만 알면 누구나 POST를 보낼 수 있어요. 그대로 받으면 가짜 이벤트가 DB에 들어가고, 사용자 화면에는 결제되지 않은 주문이 완료로 뜨죠. 문을 잠그는 방법은 3단계면 충분해요.

서명검증: 원문으로 계산하고 timingSafeEqual로 비교

보내는 쪽과 받는 쪽이 시크릿을 공유하고, 본문을 HMAC-SHA256으로 서명해 헤더에 담아 보내요. 직접 만들어 보니 함정은 딱 하나였어요. 서명은 파싱된 JSON이 아니라 원문 문자열로 계산해야 해요. 그래서 request.json() 대신 request.text()를 먼저 받아요.

import { createHmac, timingSafeEqual } from "node:crypto";
 
const raw = await request.text();
const expected = createHmac("sha256", secret).update(raw).digest("hex");
const received = request.headers.get("x-signature") ?? "";
const ok =
  expected.length === received.length &&
  timingSafeEqual(Buffer.from(expected), Buffer.from(received));

비교에 === 대신 timingSafeEqual을 쓰는 이유가 있어요. 문자열 비교의 미세한 시간 차이로 서명을 추측하는 공격을 막아 줘요. 두 값의 길이가 다르면 예외가 나니 길이 확인을 앞에 둬요.

재전송 방지: 타임스탬프에 5분 창을

서명이 유효해도, 오간 요청을 통째로 복사해 다시 보내면 그대로 통과해요. 그래서 타임스탬프를 서명 대상에 포함하고, 받은 시각과 5분(300초) 넘게 차이 나면 거절해요. 적용해 봤더니 헤더 2개(서명·타임스탬프)만 추가하면 끝나는 일이었어요.

멱등 처리: 같은 이벤트는 화면에 한 번만

외부 서비스는 응답이 늦으면 같은 이벤트를 다시 보내요. 이건 정상 동작이에요. 받는 쪽이 대비를 안 하면 알림이 두 번 가고, 목록 화면에는 같은 행이 두 줄 생겨요. 이벤트 ID를 저장해 두고 이미 처리한 ID면 200으로 조용히 응답하면 돼요. 테스트해 보면 같은 이벤트를 세 번 보내도 화면에는 한 번만 반영돼요.

서명검증타임스탬프 확인멱등 키 조회처리

화면 쪽에서도 할 일이 있어요. 어드민에 웹훅 수신 로그를 만들 때 실패 사유(서명 불일치·시간 초과·중복)를 상태 배지로 구분해 두면 연동 장애를 빨리 알아채요. 돌려 보니 이 배지 하나로 디버깅이 훨씬 짧아졌어요.

  • 원문으로 서명 계산
  • 5분 타임스탬프 창
  • 이벤트 ID 멱등 키

농구 동호회 회비를 관리하는 플랜비(plan-b)를 만들면서도 외부 연동을 검토할 땐 이 3종 세트부터 확인해요. 혼자 운영하면 장애 대응도 혼자라, 들어오는 문부터 잠그는 게 결국 반복을 줄이는 길이더라고요.

새 연동을 붙이기 전에 서명·시간·중복, 이 세 가지를 체크리스트로 두고 시작해 보세요. 보안은 거창한 인프라가 아니라 습관에 가까워요.

#웹훅#Next.js#보안

라이프케어로그 서비스가 궁금하신가요?

AI 기반 건강·일정·재활 관리 앱을 직접 써보세요.

서비스 살펴보기

관련 글

댓글

아직 댓글이 없어요. 첫 댓글을 남겨주세요.