PHP 웹훅 서명 검증: 순수 PHP와 Laravel
PHP에서 Sume 웹훅 검증하기: timestamp.raw_body에 hash_hmac sha256을 적용하고, sume-v1 항목을 나눠 각각 hash_equals로 비교하세요.

PHP에서 Sume 웹훅 서명을 검증하려면 file_get_contents('php://input')(Laravel에서는 $request->getContent())로 원본 본문을 읽고, 재전송 허용 시간을 벗어난 타임스탬프를 거부한 뒤, hash_hmac('sha256', $timestamp . '.' . $raw, $secret)를 계산하세요. 다이제스트 앞에 sume-v1=를 붙이고, x-sume-webhook-signature를 쉼표로 나눈 각 항목과 hash_equals로 비교하되 알려진 문자열을 먼저 넘기세요.
PHP 관련 내용은 PHP 매뉴얼의 hash_hmac, hash_equals, php:// 페이지에서, Laravel 관련 내용은 Laravel 12.x의 요청, CSRF 보호, 라우팅, 설정 문서와 Symfony의 HttpFoundation 페이지에서 가져왔습니다. Sume 관련 내용은 Run 웹훅 (영문), 웹훅 (영문), 웹훅 검증에서 가져왔습니다. 모두 2026-09-27에 확인했습니다. Sume는 PHP 패키지를 제공하지 않습니다. SDK는 TypeScript용이며, 문서에 다른 언어로 수신기를 만드는 경우를 위한 서명 방식이 자세히 나와 있습니다. 전달 규약은 Sume 영상 실행용 서명된 웹훅에 있습니다.
Sume 서명은 PHP 함수와 어떻게 대응하나요?
Sume 검증의 단계마다 그대로 대응하는 PHP 호출이 있습니다.
| 단계 | Sume 규칙 | PHP |
|---|---|---|
| 원본 본문 | JSON을 파싱하기 전에 원본 바이트를 검증 | file_get_contents('php://input'), Laravel에서는 $request->getContent() |
| 헤더 | x-sume-webhook-timestamp와 x-sume-webhook-signature | $_SERVER['HTTP_X_SUME_WEBHOOK_TIMESTAMP']와 $_SERVER['HTTP_X_SUME_WEBHOOK_SIGNATURE'], Laravel에서는 헤더가 없으면 null을 반환하는 $request->header() |
| 다이제스트 | <timestamp>.<raw_body>에 대한 HMAC-SHA256, hex | hash_hmac('sha256', $data, $secret), $binary가 true가 아니면 소문자 hex |
| 비교 | 상수 시간, sume-v1= 항목 중 하나라도 일치하면 수락 | explode(',', $header)의 각 항목에 hash_equals($expected, $entry) |
| 재전송 허용 시간 | 벗어난 타임스탬프는 거부. 오 분이 무난한 기본값 | abs(time() - (int) $ts) > 300 |
순수 PHP로 Sume 웹훅은 어떻게 검증하나요?
hash_equals는 타이밍 공격에 안전한 PHP의 문자열 비교 함수입니다. 매뉴얼은 일반 === 비교가 문자열이 어디서 달라지는지에 따라 걸리는 시간이 달라진다고 설명하며, 사용자가 제공한 문자열을 두 번째 매개변수로 넘기라고 경고합니다. 아래 함수는 모든 항목을 확인합니다. 시크릿을 교체한 뒤 24시간 동안은 헤더에 유효한 시크릿마다 sume-v1= 항목이 하나씩 최신순으로 실리기 때문입니다. 또 Sume의 TypeScript 검증기처럼 빈 시크릿을 거부하므로, 설정이 빠져 있어도 누구나 계산할 수 있는 빈 HMAC 키가 되지 않습니다.
<?php
function sume_verify(string $raw, ?string $ts, ?string $header, string $secret): bool
{
if ($secret === '' || $ts === null || $header === null || !ctype_digit($ts)) return false;
if (abs(time() - (int) $ts) > 300) return false; // five-minute replay window
$expected = 'sume-v1=' . hash_hmac('sha256', (int) $ts . '.' . $raw, $secret);
$matched = false;
foreach (explode(',', $header) as $entry) { // two entries during a rotation
// Known string first, user-supplied string second; check every entry.
$matched = hash_equals($expected, trim($entry)) || $matched;
}
return $matched;
}
$raw = file_get_contents('php://input'); // raw body, before json_decode
if (!sume_verify($raw, $_SERVER['HTTP_X_SUME_WEBHOOK_TIMESTAMP'] ?? null,
$_SERVER['HTTP_X_SUME_WEBHOOK_SIGNATURE'] ?? null,
(string) getenv('SUME_COM_WEBHOOK_SIGNING_SECRET'))) {
http_response_code(401);
exit;
}
record_once(json_decode($raw, true)); // your table or queue, keyed on request_id or job_id
http_response_code(204);Laravel에서는 어떻게 받나요?
Laravel의 ValidateCsrfToken 미들웨어는 기본적으로 web 미들웨어 그룹에서 실행되는데, 웹훅을 보내는 쪽은 여러분의 CSRF 토큰을 알 수 없습니다. Laravel은 웹훅 라우트를 web 그룹 밖에 두거나, bootstrap/app.php의 validateCsrfTokens(except: [...])에 해당 URI를 나열하라고 권합니다. php artisan install:api로 만들어지는 routes/api.php의 라우트는 상태가 없고(stateless), api 그룹에 속하며, /api 아래에서 서비스됩니다.
Illuminate\Http\Request는 Symfony의 Request를 확장하며, 그 getContent()는 원본 본문을 반환합니다. 시크릿은 .env에 두고 config/services.php에서 env('SUME_COM_WEBHOOK_SIGNING_SECRET')로 읽은 뒤, 라우트에서는 config()를 쓰세요. sume_verify는 헬퍼 파일에서 불러옵니다.
<?php // routes/api.php: stateless, api group, served at /api/hooks/sume
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
Route::post('/hooks/sume', function (Request $request) {
$raw = $request->getContent(); // the raw body, before any JSON parsing
$ok = sume_verify(
$raw,
$request->header('X-Sume-Webhook-Timestamp'), // null when absent
$request->header('X-Sume-Webhook-Signature'),
(string) config('services.sume.webhook_secret'),
);
if (!$ok) {
return response('', 401);
}
record_once(json_decode($raw, true)); // then hand slow work to a queue
return response('', 204);
});왜 모든 서명 검증이 실패하나요?
다음 PHP·Laravel 실수부터 확인하세요.
- 본문을 다시 인코딩한 경우.
json_encode(json_decode($raw))는 Sume가 서명한 값이 아닙니다. 키 순서와 공백도 서명된 바이트의 일부이기 때문입니다.$raw를 검증한 다음 디코딩하세요. hash_hmac에$binary = true를 넘긴 경우. 이러면sume-v1=뒤에 오는 소문자 hex 대신 원시 바이트가 반환됩니다.- 설정 파일 밖에서
env()를 호출한 경우.php artisan config:cache를 실행한 뒤에는 Laravel이 더 이상.env를 로드하지 않고env()는 시스템 수준 환경 변수만 반환하므로, 시크릿이 빈 값으로 돌아와sume_verify가 모든 전달을 거부합니다. - 헤더나 시크릿 문제. 헤더 전체를 비교하면 시크릿 교체 기간의 모든 전달에서 실패하며, 시크릿이 맞지 않는지는
x-sume-webhook-secret-fingerprint헤더로 드러납니다. 두 가지 모두 웹훅 전달 디버깅에서 다룹니다.
검증한 뒤 엔드포인트는 무엇을 해야 하나요?
이벤트를 기록하고, Sume의 10초 시도 시간 안에 204로 응답한 뒤, 느린 작업은 큐에 넘기세요. 실행은 request_id로, Job은 job_id로 중복을 제거합니다. 나머지 전달 규칙은 Sume 영상 실행용 서명된 웹훅에서 다루며, Go 버전은 같은 검증을 hmac.Equal로 수행합니다.
출처
- Run 웹훅 (영문)
- 웹훅 (영문)
- 웹훅 검증
- TypeScript SDK
- Format 쿡북
- PHP 매뉴얼: hash_hmac (2026-09-27 확인)
- PHP 매뉴얼: hash_equals (2026-09-27 확인)
- PHP 매뉴얼: php:// 래퍼 (2026-09-27 확인)
- PHP 매뉴얼: $_SERVER (2026-09-27 확인)
- Laravel 12.x: HTTP 요청 (2026-09-27 확인)
- Laravel 12.x: CSRF 보호 (2026-09-27 확인)
- Laravel 12.x: 라우팅 (2026-09-27 확인)
- Laravel 12.x: 설정 (2026-09-27 확인)
- Symfony: HttpFoundation 컴포넌트 (2026-09-27 확인)
관련 글
연동 카테고리의 다른 글
- Pipedream에서 Sume 영상 실행의 웹훅 콜백 기다리기
Sume 영상 실행을 시작하는 단계에서 $.flow.suspend()를 호출하고 resume_url을 webhook_url로 넘기면, Sume가 결과를 POST할 때 Pipedream이 재개합니다.
- Power Automate HTTP 요청 API: Sume 실행 시작과 폴링
Power Automate HTTP action으로 Sume API를 호출하세요. Format 실행을 시작하고, Do until 루프로 폴링하고, 키는 Key Vault 시크릿에서 읽습니다.
- Pydantic AI MCP 서버: 에이전트에 Sume 호스팅 도구 연결
MCPToolset과 API 키 헤더로 Pydantic AI 에이전트를 Sume 호스팅 MCP 서버에 연결하고, 도구를 걸러 내고, 유료 호출은 승인 전까지 보류하세요.
- Shopify 제품 영상 AI API: products/create 웹훅
Shopify products/create 웹훅에 오 초 안에 응답하고, 큐에서 Sume Format을 실행한 뒤 staged upload로 MP4를 Shopify에 올리세요.
작성자 Sume