Belgelere geç
PHP

Bir teslimatı doğrulamak

`OpenEmail::verifyWebhookSignature`: sabit zamanlı, yeniden oynatma penceresiyle ve ayrıştırılmış olayı geri döndürerek.

Bir istek işleyicisinde

Bir webhook URL'si herkese açıktır. İnternetteki her şey ona doğru biçimli JSON'u POST ile gönderebilir; bu yüzden imzayı denetlemeden olayın type değerini okuyan bir işleyici, herkese açık bir yazma API'sidir.

webhook.php
use OpenEmail\Exception\WebhookSignatureException;use OpenEmail\OpenEmail; $payload = (string) file_get_contents('php://input'); try {    $event = OpenEmail::verifyWebhookSignature(        $payload,        $_SERVER,        (string) getenv('OPENEMAIL_WEBHOOK_SECRET'),        toleranceSeconds: 300,    );} catch (WebhookSignatureException) {    http_response_code(400);     return;} error_log($event['type'] . ' ' . $event['id']);http_response_code(204);

HAM gövdeyi bir dize olarak geçirin. Onu çözüp yeniden kodlamak anahtar sırasını ve boşlukları değiştirir ve imza eşleşmez; yükün string türünde tanımlanmasının nedeni budur: json_decode() sonucu ya da bir framework'ün ayrıştırdığı girdi gibi bir dizi, denetlenmek yerine bir TypeError olur. Başlıklar; getallheaders() çağrısının döndürdüğü gibi başlık adı herhangi bir büyük/küçük harf biçiminde olan bir dizi, başlığın HTTP_X_OPENEMAIL_SIGNATURE olarak geldiği $_SERVER, Symfony ya da Laravel HeaderBag gibi herhangi bir yinelenebilir nesne ya da bir PSR-7 isteği olabilir. Dizi olan bir değer ilk öğesinden okunur.

Elle yazılmış bir denetimin genellikle ele almadığı iki şeyi ele alır: MAC'i hash_equals() ile sabit zamanda karşılaştırır, böylece doğru önek süre ölçülerek elde edilemez; ve her iki yönde de toleranceSeconds: değerinden (aksini belirtmedikçe beş dakika) daha eski bir teslimatı reddeder, böylece yakalanmış bir istek sonsuza dek yeniden oynatılamaz. toleranceSeconds: 0 yeniden oynatma denetimini kapatır. İki hata da sessizdir. Bunlardan birine sahip bir işleyici, yazmayı düşüneceğiniz her testi geçer.

Her başarısızlıkta OpenEmail\Exception\WebhookSignatureException fırlatır: eksik bir X-OpenEmail-Signature başlığı, t=<seconds>,v1=<hex> biçiminde olmayan bir başlık, pencerenin dışındaki bir zaman damgası, eşleşmeyen bir imza ya da JSON nesnesi olmayan imzalı bir gövde. Başarılı olduğunda gövdeyi id, type, createdAt ve data içeren, camelCase anahtarlı bir diziye çözülmüş olarak döndürür; böylece yanlış yapılabilecek ikinci bir json_decode() olmaz.

Boş bir gizli anahtar ise bunun yerine OpenEmail\Exception\InvalidArgumentException fırlatır, çünkü bu kötü bir teslimat değil, yapılandırmanızdaki bir hatadır; getenv() önündeki (string) de ayarlanmamış bir değişkeni tam olarak buna dönüştürür. Yalnızca WebhookSignatureException yakalayın ve 400 ile yanıt verin. Böylece eksik bir gizli anahtar betiği yakalanmamış bir istisnayla sonlandırır, üretimdeki PHP buna 500 ile yanıt verir ve her olay sahte diye geri çevrilmek yerine, siz sorunu düzelttiğinizde teslimat yeniden denenir.

Denetim, her PHP derlemesinde bulunan hash_hmac() ve hash_equals() üzerinde çalışır; bu yüzden hiçbir eklenti gerektirmez. Örnek exit çağırmak yerine return kullanır; böylece aynı satırlar bir fonksiyonun ya da bir ön denetleyicinin (front controller) içinde de çalışır.

Bir PSR-7 uygulamasında

Slim ve Mezzio'nun size verdiği gibi bir PSR-7 isteğiyle, isteğin kendisini başlıklar olarak, gövdesini de bir dize olarak geçirin.

webhook_handler.php
use OpenEmail\Exception\WebhookSignatureException;use OpenEmail\OpenEmail;use Psr\Http\Message\ResponseFactoryInterface;use Psr\Http\Message\ResponseInterface;use Psr\Http\Message\ServerRequestInterface; function receiveOpenEmail(ServerRequestInterface $request, ResponseFactoryInterface $responses): ResponseInterface{    try {        $event = OpenEmail::verifyWebhookSignature(            (string) $request->getBody(),            $request,            (string) getenv('OPENEMAIL_WEBHOOK_SECRET'),        );    } catch (WebhookSignatureException) {        return $responses->createResponse(400);    }     error_log($event['type'] . ' ' . $event['id']);     return $responses->createResponse(204);}

Hızlı yanıt verin: 5 saniye içinde yanıt almayan bir teslimat başarısız sayılır ve daha sonra yeniden gönderilir; bu yüzden işi bir kuyruğa devredin ve 2xx ile yanıt verin. Kuyruğa devretme dahil Laravel ve Symfony denetleyicileri framework'ler sayfasındadır.

OpenEmail\Constants\WebhookSignatureHeaders bir teslimatın taşıdığı üç başlığı adlandırır: X-OpenEmail-Signature, olayın türüyle X-OpenEmail-Event ve kimliğiyle, yani gövdedeki aynı id ile X-OpenEmail-Delivery. Bu kimlik olayın her yeniden denemesinde ve yeniden oynatmasında aynı kalır; bu yüzden daha önce işlediğiniz olayları atlarken saklamanız gereken budur.

Gizli anahtar döndürülürken

webhooks->rotateSecret için bir çakışma penceresi yoktur: teslimatlar, çağrı döndüğü andan itibaren yeni gizli anahtarla imzalanır. Önce iki gizli anahtarı da kabul eden bir alıcı dağıtın, döndürün, yeni gizli anahtarı alıcının okuduğu yere kaydedin, ardından eskisini kaldırın.

two_secrets.php
use OpenEmail\Exception\WebhookSignatureException;use OpenEmail\OpenEmail; function verifyDelivery(string $payload, mixed $headers): array{    $next = getenv('OPENEMAIL_WEBHOOK_SECRET_NEXT');     try {        return OpenEmail::verifyWebhookSignature($payload, $headers, (string) getenv('OPENEMAIL_WEBHOOK_SECRET'));    } catch (WebhookSignatureException $error) {        if (!is_string($next) || $next === '') {            throw $error;        }         return OpenEmail::verifyWebhookSignature($payload, $headers, $next);    }}

webhooks->test imzalı yapay bir olay gönderir; bu yüzden eskisini kaldırmadan önce yeni gizli anahtarın doğrulandığını kanıtlamak için onu döndürmeden sonra çağırın.