lifecarelog
백엔드

결제 webhook 소비자 하드닝: HMAC·replay·멱등 3겹 방어

결제 webhook을 안전하게 받으려면 HMAC 서명검증, replay 윈도우, 멱등 처리 세 겹이 필요해요. 백엔드에서 직접 붙이며 정리한 순서와 함정을 담았어요.

5분 읽기

결제 webhook, 200만 돌려주면 끝이 아니에요

혼자 백엔드를 운영하다 보면 외부에서 날아오는 webhook을 받는 엔드포인트를 하나쯤 열게 돼요. 결제 승인, 구독 갱신, 정산 알림 같은 것들이요. 그런데 이 엔드포인트는 인터넷 아무 데서나 POST를 던질 수 있는 열린 문이에요. "누가 보냈는지"부터 확인하지 않으면, 가짜 결제 완료 한 건에 데이터가 통째로 흔들릴 수 있어요.

직접 붙여보니 받는 쪽(consumer) 하드닝은 크게 세 겹으로 정리되더라고요.

HMAC 서명검증타임스탬프 검증(replay 윈도우)멱등 처리(중복 무시)

1겹: HMAC 서명검증으로 발신자 확인

결제사는 요청 본문을 공유 시크릿으로 서명해서 헤더에 실어 보내요. 받는 쪽은 같은 시크릿으로 본문을 HMAC-SHA256(256비트)으로 다시 계산해 헤더 값과 비교해요. 여기서 함정이 두 개예요. 하나는 파싱된 JSON이 아니라 받은 그대로의 raw body로 서명해야 한다는 점이에요. 프레임워크가 body를 먼저 파싱해버리면 바이트가 미묘하게 달라져서 검증이 계속 실패해요. 처음엔 이걸 몰라서 한참 헤맸어요. 다른 하나는 문자열 == 대신 constant-time 비교를 써야 한다는 거예요. 일반 비교는 얼마나 일치하는지가 응답 시간에 새어나가서, 밀리초 차이로 시크릿을 조금씩 맞춰가는 공격에 약해요.

2겹: replay 윈도우로 재전송 차단

서명만 맞으면 될 것 같지만, 과거의 진짜 요청을 그대로 가로채 다시 던지는 replay가 남아요. 서명은 여전히 유효하니까요. 그래서 함께 오는 타임스탬프를 봐요. 지금 시각과 5분(300초) 넘게 벌어지면 오래된 요청으로 보고 거절해요. 이 300초는 서버 시계 오차와 네트워크 지연을 감안한 값이라, 너무 좁게 잡으면 정상 요청까지 떨어져요. 실제 로그를 보고 조정하는 게 좋아요. 타임스탬프도 서명 대상에 포함돼야 위조를 막을 수 있어요.

3겹: 멱등 처리로 중복을 흡수

webhook은 받는 쪽이 200을 줄 때까지 여러 번 재시도해요. 응답이 조금만 늦어도 같은 이벤트가 2번, 3번 도착하는 게 정상 동작이에요. 그래서 이벤트마다 붙어오는 고유 ID를 DB에 unique 제약으로 저장하고, 이미 있는 ID면 처리를 건너뛰고 그냥 200을 돌려줘요. 적용해 봤더니 같은 금액이 두 번 반영되는 사고를 이 한 겹이 막아줬어요. 처리와 ID 저장은 같은 트랜잭션으로 묶어야, 처리 도중 죽었을 때 반쪽만 남지 않아요.

  • raw body 서명
  • constant-time 비교
  • 300초 윈도우
  • unique 제약 멱등
  • 검증 후 200

200은 맨 마지막에

200 응답은 세 겹을 다 통과한 뒤에 돌려줘요. 검증이 끝나기도 전에 먼저 200을 주면, 결제사는 "잘 받았다"고 여기고 재시도를 멈춰버려서 놓친 이벤트를 되살릴 방법이 사라져요.

라이프케어로그에서 플랜비(plan-b) 같은 서비스를 혼자 만들다 보면, 이런 외부 이벤트 처리 한 겹 한 겹이 결국 데이터 신뢰로 이어져요. 화려하진 않지만, 돈이 오가는 경계일수록 이 세 겹은 빼먹지 않는 게 마음이 편하더라고요.

#webhook#HMAC#멱등성#백엔드보안#결제연동

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

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

서비스 살펴보기

관련 글

댓글

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