Webhook Alıcısı Tasarımı: İmza Doğrulama, Tekrar Teslim ve Güvenli İşleme
Webhook alıcısı tasarımı: HMAC imza doğrulama, zaman damgası penceresi, SQLite gelen kutusuyla tekrar teslimi tekilleştirme ve hızlı cevap sonra işleme deseni.
Webhook alıcısı tasarımı, ödeme sağlayıcısından, e-ticaret platformundan ya da form aracından gelen olay bildirimlerini güvenle karşılayan ve kaybetmeden işleyen uç noktayı kurma işidir. Küçük ve orta ölçekli işletmelerde webhook çoğu zaman on satırlık bir Flask ya da Express ucu olarak başlar: istek gelir, veri tabanına yazılır, iş biter. Sorun, aynı olay iki kez geldiğinde, imzası doğrulanmamış bir istek sahte sipariş oluşturduğunda ya da işleyici takıldığı için gönderici vazgeçtiğinde ortaya çıkar.
Gönderici tarafındaki yeniden deneme, geri çekilme ve hız limiti desenleri ayrı bir yazının konusu; burada masanın öbür tarafındayız. Alıcı üç şeyi garanti etmelidir: isteğin gerçekten beklenen kaynaktan geldiğini, aynı olayın yalnız bir kez etki ettiğini ve göndericinin zaman aşımına düşmeden cevap aldığını.
Bu yazıda dört bölümde bu garantileri kuruyoruz: hızlı cevap ve arka planda işleme, HMAC imza doğrulama ve tekrar oynatma penceresi, olay kimliğiyle tekilleştirme, güvenlik ve kişisel veri sınırları. Kod örnekleri standart kütüphaneyle çalışır; hmac ve sqlite3 dışında bağımlılık yoktur.
Önce kaydet ve cevapla, sonra işle
Hızlı cevap ilkesi, webhook alıcısının gelen isteği doğrulayıp ham haliyle kaydettikten sonra hemen başarı kodu döndürmesi, asıl iş mantığını ise ayrı bir işleyiciye bırakmasıdır. Gönderici sunucular cevap için sınırlı süre bekler; sağlayıcı belgelerine göre bu süre genellikle birkaç saniye ile yarım dakika arasındadır ve aşıldığında istek başarısız sayılıp yeniden gönderilir. İstek döngüsünde yapılan uzun işleme, cevap gecikmesine ve gereksiz tekrarlara yol açar.
Kaydet, cevapla, işle sırası şu adımlarla kurulur:
- İstek gövdesi ham bayt olarak okunur ve imza bu bayt dizisi üzerinde doğrulanır; JSON'a çevrilip yeniden serileştirilmiş gövde üzerinde imza tutmaz.
- Doğrulanan olay, kimliği ve alınma zamanıyla bir gelen kutusu tablosuna yazılır; ardından göndericiye 200 kodu döner.
- Ayrı bir süreç ya da zamanlanmış iş, gelen kutusundaki işlenmemiş olayları sırayla alır, iş mantığını çalıştırır ve durumu günceller.
- İşleyici hata verirse olay kutuda kalır; kayıt yerinde durduğu için bir sonraki turda yeniden denenir ve hiçbir olay kaybolmaz.
Bu düzen webhook alıcısını ayrı bir kuyruk sistemi kurmadan dayanıklı kılar. Gelen kutusu bir SQLite dosyasında durabilir; işleyici her dakika çalışan bir cron işidir ve zamanlanmış iş tasarımı yazısındaki kilit ve yeniden çalıştırma kuralları burada da geçerlidir.
HMAC imza doğrulama ve tekrar oynatma penceresi
HMAC imza doğrulama, göndericinin paylaşılan gizli anahtarla istek gövdesinden ürettiği özet değerini, alıcının aynı yöntemle yeniden hesaplayıp karşılaştırmasıdır. Sağlayıcılar imzayı bir başlıkta, çoğu zaman zaman damgasıyla birlikte gönderir; alıcı imzayı doğrulamadan gövdeye güvenmez. IP adresi listesiyle kısıtlama imzanın yerini tutmaz, çünkü sağlayıcıların adresleri değişir ve ara katmanlar kaynağı gizler.
Karşılaştırma sabit zamanlı yapılır. Sıradan eşitlik denetimi ilk farklı baytta durur ve yanıt süresi imzanın ne kadarının doğru olduğunu sızdırır; hmac.compare_digest her durumda aynı sürede çalışır. Zaman damgası da imzaya dahil edilir ve alıcı belirli bir pencerenin dışındaki istekleri reddeder; böylece ele geçirilmiş geçerli bir istek daha sonra yeniden oynatılamaz.
Aşağıdaki doğrulayıcı, Stripe ve benzeri sağlayıcıların kullandığı desene yakın çalışır: zaman damgası ve ham gövde noktayla birleştirilir, SHA-256 ile HMAC hesaplanır, onaltılık özet karşılaştırılır. Ana blok üç durumu sınar: geçerli istek, değiştirilmiş gövde ve eski zaman damgası.
import hashlib
import hmac
import time
PENCERE_SANIYE = 300
def imzala(gizli, zaman, govde):
veri = ("%d." % zaman).encode() + govde
return hmac.new(gizli, veri, hashlib.sha256).hexdigest()
def dogrula(gizli, govde, baslik_imza, baslik_zaman, simdi=None):
"""govde ham bayt dizisidir; başlık değerleri metin ya da sayı olarak gelir."""
simdi = int(time.time()) if simdi is None else simdi
try:
zaman = int(baslik_zaman)
except (TypeError, ValueError):
return False, "zaman damgası okunamadı"
if abs(simdi - zaman) > PENCERE_SANIYE:
return False, "zaman damgası pencere dışında"
beklenen = imzala(gizli, zaman, govde)
if not hmac.compare_digest(beklenen, (baslik_imza or "").strip().lower()):
return False, "imza uyuşmuyor"
return True, "ok"
if __name__ == "__main__":
gizli = b"whsec_ornek_gizli_anahtar"
govde = b'{"id": "evt_1", "type": "order.paid", "amount": 1250}'
zaman = int(time.time())
imza = imzala(gizli, zaman, govde)
print(dogrula(gizli, govde, imza, zaman))
print(dogrula(gizli, govde + b" ", imza, zaman))
print(dogrula(gizli, govde, imza, zaman - 3600))
Gizli anahtar ortam değişkeninden ya da sır kasasından okunur; koda ve depoya yazılmaz. Anahtar yenilenirken alıcı kısa bir süre iki anahtarı da kabul eder: doğrulama önce yeni, sonra eski anahtarla denenir; göndericide değişiklik tamamlanınca eski anahtar kaldırılır.
Olay kimliğiyle tekilleştirme ve gelen kutusu tablosu
Tekilleştirme (deduplication), her olayın göndericinin verdiği benzersiz kimlikle kaydedilmesi ve aynı kimlik ikinci kez geldiğinde işlemin yinelenmemesidir. Webhook sağlayıcıları teslimatı en az bir kez garanti eder, tam bir kez değil; ağ hatası, zaman aşımı ya da alıcının geç cevabı aynı olayın birkaç kez gelmesine yol açar. Kimlik sütununa konan benzersizlik kısıtı bu tekrarı veri tabanı düzeyinde durdurur.
Gelen kutusu tablosu dört sütunla yeterlidir: olay kimliği, olay türü, ham gövde ve durum. Durum alındı, işlendi ya da hata değerlerini alır; işleyici yalnız alındı durumundakileri seçer. Ham gövdenin saklanması hata ayıklamayı kolaylaştırır ve iş mantığı değiştiğinde eski olayların yeniden işlenmesine izin verir. Sağlayıcı olay kimliği vermiyorsa gövdenin SHA-256 özeti kimlik olarak kullanılır.
import hashlib
import json
import sqlite3
VT = "webhook_gelen.sqlite"
def tablo(baglanti):
baglanti.execute(
"CREATE TABLE IF NOT EXISTS gelen ("
"olay_id TEXT PRIMARY KEY, tur TEXT, govde TEXT, "
"durum TEXT DEFAULT 'alindi', alinma TEXT DEFAULT CURRENT_TIMESTAMP)"
)
def kaydet(baglanti, govde):
"""Doğrulanmış ham gövdeyi gelen kutusuna yazar; tekrar teslimde False döner."""
veri = json.loads(govde)
olay_id = veri.get("id") or hashlib.sha256(govde).hexdigest()
imlec = baglanti.execute(
"INSERT OR IGNORE INTO gelen (olay_id, tur, govde) VALUES (?, ?, ?)",
(olay_id, veri.get("type", ""), govde.decode("utf-8")),
)
baglanti.commit()
return imlec.rowcount == 1
def isle(baglanti, isleyiciler):
satirlar = baglanti.execute(
"SELECT olay_id, tur, govde FROM gelen WHERE durum = 'alindi' ORDER BY alinma"
).fetchall()
for olay_id, tur, govde in satirlar:
try:
isleyiciler.get(tur, lambda v: None)(json.loads(govde))
yeni = "islendi"
except Exception as hata:
print("hata", olay_id, hata)
yeni = "hata"
baglanti.execute("UPDATE gelen SET durum = ? WHERE olay_id = ?", (yeni, olay_id))
baglanti.commit()
return len(satirlar)
if __name__ == "__main__":
vt = sqlite3.connect(VT)
tablo(vt)
olay = b'{"id": "evt_1", "type": "order.paid", "amount": 1250}'
print("ilk teslim yeni mi:", kaydet(vt, olay))
print("tekrar teslim yeni mi:", kaydet(vt, olay))
isleyiciler = {"order.paid": lambda v: print("sipariş işlendi, tutar:", v["amount"])}
print("işlenen olay:", isle(vt, isleyiciler))
print(vt.execute("SELECT olay_id, durum FROM gelen").fetchall())
Tekrar kadar sıra da sorun çıkarır. Sağlayıcı olayları üretildikleri sırayla teslim etmeyi vaat etmez; ödeme tamamlandı olayı, sipariş oluşturuldu olayından önce gelebilir. İşleyici bu yüzden olayın geliş sırasına değil, gövdedeki üretim zamanına ve nesnenin güncel durumuna göre karar verir. Şüphe halinde nesne sağlayıcının API'sinden yeniden okunur; olay bir tetik, kaynak ise sağlayıcının kendi kaydıdır.
İş mantığının kendisi de tekrarlı çalıştırılabilir yazılır: sipariş kaydı varsa güncellenir, yoksa eklenir; bildirim e-postası gönderildiyse ikinci kez gönderilmez. Gelen kutusu tekrarın büyük bölümünü keser, ama işleyici yarıda kesilip yeniden başladığında aynı olayı ikinci kez görebilir; son savunma iş mantığının içindedir.
Güvenlik sınırları ve kişisel veri
Webhook güvenliği, imza doğrulamanın ötesinde uç noktanın yalnız HTTPS üzerinden dinlemesi, gövde boyutunun sınırlanması, gelen verinin şemaya karşı doğrulanması ve hiçbir alanın doğrulanmadan iş mantığına ya da veri tabanı sorgusuna girmemesidir. Alıcı, göndericinin ilettiği her şeyi dış girdi olarak ele alır; tutar, müşteri kimliği ya da ödeme durumu gibi kritik alanlar sağlayıcının API'sinden yeniden okunarak teyit edilir.
Ödeme ve sipariş webhook'ları çoğu zaman ad, e-posta ve adres gibi kişisel veri taşır. KVKK açısından bu veri alındığı anda sorumluluk alıcıya geçer: ham gövde loglara ve hata izleme araçlarına yazılmaz, gelen kutusu tablosundaki kayıtlar tanımlı bir saklama süresi sonunda silinir ya da kişisel alanlar maskelenir, tabloya erişim en az yetkiyle sınırlanır. İşleme amacı dışındaki alanlar hiç saklanmaz.
Son olarak alıcının sessizce durmasına karşı bir nöbetçi gerekir. Sağlayıcı tarafında uç nokta kapatılmış, sertifika süresi dolmuş ya da sunucu düşmüş olabilir; hiçbir hata görünmez, yalnız olaylar gelmez. Belirli bir süre boyunca hiç olay alınmadığında alarm veren bir denetim, heartbeat izleme yazısındaki desenle kurulur.
Sonuç: doğrula, kaydet, cevapla, sonra işle
Güvenilir webhook alıcısı dört adımın sırasıyla tanımlanır: ham gövde üzerinde imza ve zaman damgası doğrulanır, olay kimliğiyle gelen kutusuna yazılır, göndericiye hemen başarı kodu dönülür, iş mantığı ayrı bir işleyicide tekrarlı çalıştırılabilir biçimde yürütülür. Bu sıra sahte isteği kapıda durdurur, tekrar teslimi zararsız kılar ve göndericinin zaman aşımına düşmesini engeller.
İlk adım olarak mevcut alıcınızda imza doğrulamanın ham bayt üzerinde ve sabit zamanlı karşılaştırmayla yapıldığını kontrol edin; ardından iş mantığını istek döngüsünden çıkarıp gelen kutusu tablosuna taşıyın. Tekrar teslim testini kendiniz yapın: aynı olayı iki kez gönderip yalnız bir kaydın etkilendiğini görün.
Ödeme, e-ticaret ya da form entegrasyonlarınızı bu düzene taşımak için iletişim bölümünden yazabilirsiniz.