İçeriğe geç

Webhook Almak — Ödeme Tamamlandı Diyen Gerçekten Sağlayıcı mı?

Orta 8 dk Sık karşılaşılır

Önce şunu oku: Outbox ve Idempotent Consumer — Olay Kaybolmasın, İki Kez de İşlenmesin

30 saniyede özet

Ödeme sağlayıcısı bir olayı webhook ile haber verir. Adresi bulan herkes de aynı adrese istek atabilir, sağlayıcı da aynı olayı ikinci kez gönderebilir. Önce HMAC imzasını doğrula, sonra olay kimliğine bak.

Bir şirketin resepsiyonunda bir mektup kutusu var. Kutuya herkes mektup atabilir. Bankadan geldiğini mühür kanıtlar; aynı numaralı mektubun ikinci kez gelmesi ise kuryenin yeniden denemesidir, yeni para değil.

  1. Bayt: Ödeme sağlayıcısı "ödeme tamamlandı" diye bizim adresimize istek atıyor. Biz de cüzdana ekliyoruz.

  2. Sen: Ama bugün cüzdana 7050 TL eklenmiş. Gerçek ödemeler 550 TL tutuyor.

  3. Bayt: Adresi bulan biri 1000 TL yazmış, biri de tutarı 50'den 5000'e çevirmiş! Bir ödeme de iki kez gelmiş.

  4. Bayt: Önce mühre bakacağız, sonra mektup numarasına.

Kim gönderdi?

Bir webhookBir olay olunca karşı sistemin senin verdiğin adrese kendiliğinden HTTP isteği atması. Sen sormazsın, o haber verir.Sözlükte gör →, karşı sistemin bir olay olunca senin adresine kendiliğinden istek atmasıdır. Ama o adres gizli bir şifre değildir; loglarda, ekran görüntülerinde ya da tahminle ortaya çıkabilir.

Güven, HMAC imzasıGövde ve iki tarafın bildiği gizli bir anahtarla hesaplanan imza. Çağrının kimden geldiğini ve yolda değişmediğini kanıtlar.Sözlükte gör → gelir. Sağlayıcı gövdeyi iki tarafın bildiği gizli bir anahtarla imzalar. Anahtarı bilmeyen doğru imzayı üretemez; gövdede tek bir bayt değişirse imza tutmaz.

Kafam karıştı, daha basit anlat

Adres herkese açık olabilir. Çağrının gerçek olduğunu imza kanıtlar.

Hızlı kontrolBaşlangıç

Webhook nedir?

Cevabı biliyor musun?Önce birini seç. Tekrar zamanlaması buna göre ayarlanıyor.

Webhook adresini kimsenin bilmemesi yeterli bir güvenlik önlemi midir?

Cevabı biliyor musun?Önce birini seç. Tekrar zamanlaması buna göre ayarlanıyor.

Kaçıncı kez geldi?

İmzayı doğruluyoruz. Sağlayıcı aynı ödemeyi, bizim cevabımız ona geç ulaştığı için ikinci kez gönderdi. İmzası doğru. Cüzdana ne olur? Cevabı göster

Aynı ödeme ikinci kez eklenir. Tekrar gelen çağrı da sağlayıcıdan gelir ve imzası doğrudur. İmza kimin gönderdiğini söyler, kaçıncı kez geldiğini değil.

Önce mühre bak, sonra numaraya.
Adım adım oku
  1. Kutuya herkes mektup atabilir.
  2. Mühür tutmuyor: sahte.
  3. Aynı numara ikinci kez: kurye tekrar denedi.
  4. Önce mühre bak, sonra numaraya.

Her olayın bir kimliği vardır. İşlenen kimlik kaydedilir; aynı kimlik yeniden gelirse para eklenmez, yalnızca “aldım” denir. Bu, idempotencyAynı işlemin bir kez de on kez de çalışsa aynı sonucu vermesi. Dağıtık sistemde timeout aldığında işlemin yapılıp yapılmadığını bilemediğin için zorunludur.Sözlükte gör →’nin webhook’taki hâlidir.

Sıra önemlidir. İmza doğrulanmadan kimlik kaydedilirse, yolda değiştirilmiş bir çağrı kimliği kapar; gerçeği gelince tekrar sanılıp atlanır.

Kafam karıştı, daha basit anlat

İmza kimin gönderdiğini, kimlik kaçıncı kez geldiğini söyler. Önce imza, sonra kimlik.

Hızlı kontrolOrta

HMAC imzası neyi kanıtlar?

Cevabı biliyor musun?Önce birini seç. Tekrar zamanlaması buna göre ayarlanıyor.

Sağlayıcı aynı olayı neden ikinci kez gönderir?

Cevabı biliyor musun?Önce birini seç. Tekrar zamanlaması buna göre ayarlanıyor.

Kendin gör

Ödeme sağlayıcısından gelen çağrılar

Tohum 32024
  1. evt_1 · 500 TL · gerçek
  2. evt_1 · 500 TL · sağlayıcının tekrarı
  3. evt_9 · 1000 TL · sahte
  4. evt_2 · 5000 TL · yolda değiştirilmiş
  5. evt_2 · 50 TL · gerçek

Cüzdana eklenen: 0 TL · olması gereken: 550 TL · ✕ yanlış: 0

Hız
Adım 0

Şu an ne oldu?

Çağrılar geliyor

Doğru toplam 550 TL. Bakalım cüzdana ne eklenecek.

Görevler0/3

  • Sahte bir çağrı cüzdana para eklesinaçık

    İpucu

    İmzayı doğrulama.

  • İmza doğru, ama aynı ödeme iki kez eklensinaçık

    İpucu

    Yalnızca imza.

  • Cüzdana tam 550 TL eklensinaçık

    İpucu

    İkisi birden.

Olay günlüğü (0)

Henüz olay yok. Oynat veya adımla.

  1. Varsayılanla oynat. Hiçbir kontrol yok: sahte, değiştirilmiş ve tekrar eden çağrılar dahil 7050 TL eklendi.
  2. Yalnızca “imzayı doğrula”yı aç. Sahteler durdu, ama tekrar gelen ödeme ikinci kez eklendi: 1050 TL.
  3. Yalnızca “kimliği atla”yı aç. Değiştirilmiş çağrı evt_2 kimliğini kaptı; gerçek 50 TL hiç eklenmedi.
  4. İkisini birden aç. Cüzdana tam 550 TL eklendi.
Hızlı kontrolOrta

Aynı olayın iki kez para eklemesini ne önler?

Cevabı biliyor musun?Önce birini seç. Tekrar zamanlaması buna göre ayarlanıyor.

İmza doğrulanmadan olay kimliği "işlendi" diye kaydedilirse ne olabilir?

Cevabı biliyor musun?Önce birini seç. Tekrar zamanlaması buna göre ayarlanıyor.

Tuzaklar

Gövdeyi ayrıştırıp sonra imzalamak. JSON’u nesneye çevirip yeniden yazmak boşlukları ve alan sırasını değiştirir. İmzayı gelen ham baytlarla doğrula.

Eski çağrıyı yeniden oynatmak. Bir hafta önceki gerçek bir çağrının imzası hâlâ geçerlidir. İmzaya dahil zaman damgasını kontrol et ve birkaç dakikadan eskisini reddet.

İşi bitirince cevap vermek. Sağlayıcı cevabı birkaç saniye bekler; geç kalırsan aynı olayı yine gönderir. İmzayı doğrula, olayı kaydet, hemen başarılı dön ve işi arka planda yap.

Kafam karıştı, daha basit anlat

Ham baytları doğrula, eski çağrıyı reddet, hızlıca “aldım” de.

Hızlı kontrolOrta

İmza neden gövdenin JSON'a çevrilmiş hâlinden değil, gelen ham baytlardan hesaplanır?

Cevabı biliyor musun?Önce birini seç. Tekrar zamanlaması buna göre ayarlanıyor.
Derinleş · Cüzdan servisi: ödeme sağlayıcısının webhook'u 4 dosya · ~102 satır · ilk okumada atlayabilirsin
Proje dosyaları

wallet/src/main/java/com/bank/wallet/webhook/ PaymentWebhookController.java Controller: gövde ham bayt olarak alınır; önce imza, sonra kimlik, sonra hızlıca 200.

wallet/src/main/java/com/bank/wallet/webhook/PaymentWebhookController.java
@RestController
@RequestMapping("/webhooks/payments")
class PaymentWebhookController {
private final SignatureVerifier verifier;
private final ReceivedEvents receivedEvents;
PaymentWebhookController(SignatureVerifier verifier, ReceivedEvents receivedEvents) {
this.verifier = verifier;
this.receivedEvents = receivedEvents;
}
@PostMapping
ResponseEntity<Void> receive(@RequestHeader("Payment-Signature") String signature,
@RequestBody byte[] rawBody) {
// 1. Who sent it? Checked on the exact bytes that arrived.
if (!verifier.isValid(signature, rawBody)) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
}
// 2. Have we seen it? Only verified events may claim an id.
PaymentEvent event = PaymentEvent.parse(rawBody);
receivedEvents.saveIfNew(event); // processed in the background
// 3. Answer quickly, so the provider has no reason to retry.
return ResponseEntity.ok().build();
}
}

wallet/src/main/java/com/bank/wallet/webhook/ SignatureVerifier.java İmza doğrulayıcı: zaman damgası ve gövdeyle HMAC-SHA256 hesaplar, sabit zamanlı karşılaştırır, eski çağrıyı reddeder.

wallet/src/main/java/com/bank/wallet/webhook/SignatureVerifier.java
@Component
class SignatureVerifier {
private static final Duration TOLERANCE = Duration.ofMinutes(5);
private final byte[] secret;
private final Clock clock;
SignatureVerifier(@Value("${payments.webhook-secret}") String secret, Clock clock) {
this.secret = secret.getBytes(StandardCharsets.UTF_8);
this.clock = clock;
}
/** Header format: "t=1767225600,v1=5f2b...". */
boolean isValid(String header, byte[] body) {
Map<String, String> parts = parse(header);
String t = parts.get("t");
String v1 = parts.get("v1");
if (t == null || v1 == null) {
return false;
}
Instant sentAt = Instant.ofEpochSecond(Long.parseLong(t));
if (Duration.between(sentAt, clock.instant()).abs().compareTo(TOLERANCE) > 0) {
return false; // a replay of an old, genuine call
}
byte[] expected = hmacSha256(t + "." + new String(body, StandardCharsets.UTF_8));
// Constant-time comparison: no timing hint about how many bytes matched.
return MessageDigest.isEqual(expected, HexFormat.of().parseHex(v1));
}
private byte[] hmacSha256(String payload) {
try {
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(secret, "HmacSHA256"));
return mac.doFinal(payload.getBytes(StandardCharsets.UTF_8));
} catch (GeneralSecurityException e) {
throw new IllegalStateException("HMAC unavailable", e);
}
}
private static Map<String, String> parse(String header) {
return Arrays.stream(header.split(","))
.map(part -> part.split("=", 2))
.filter(kv -> kv.length == 2)
.collect(Collectors.toMap(kv -> kv[0], kv -> kv[1], (a, b) -> a));
}
}

wallet/src/main/java/com/bank/wallet/webhook/ ReceivedEvents.java Olay kaydı: aynı kimlik ikinci kez eklenemez; veritabanının benzersizlik kısıtı tekrarı tanır.

wallet/src/main/java/com/bank/wallet/webhook/ReceivedEvents.java
@Repository
class ReceivedEvents {
private final JdbcClient jdbc;
ReceivedEvents(JdbcClient jdbc) {
this.jdbc = jdbc;
}
/** Stores the event once; a retry with the same id changes nothing. */
void saveIfNew(PaymentEvent event) {
jdbc.sql("""
INSERT INTO received_payment_events (event_id, amount, currency, status)
VALUES (:id, :amount, :currency, 'PENDING')
ON CONFLICT (event_id) DO NOTHING
""")
.param("id", event.id())
.param("amount", event.amount())
.param("currency", event.currency())
.update();
}
}

wallet/src/main/resources/db/migration/ V7__received_payment_events.sql Tablo: olay kimliği birincil anahtar.

wallet/src/main/resources/db/migration/V7__received_payment_events.sql
CREATE TABLE received_payment_events (
event_id VARCHAR(64) PRIMARY KEY, -- the provider's event id
amount NUMERIC(19, 2) NOT NULL,
currency CHAR(3) NOT NULL,
status VARCHAR(16) NOT NULL, -- PENDING, CREDITED
received_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

Kendini sına

Şimşek turu1/4

Webhook adresini gizli tutmak, sahte çağrıları durdurmaya yeter.

Soru 1/3Orta

Sağlayıcı cevabı 10 saniyede bekliyor ama işlem 30 saniye sürüyor. Ne yapılmalı?

Cevabı biliyor musun?Önce birini seç. Tekrar zamanlaması buna göre ayarlanıyor.

Aklında kalacak üç şey

  1. 1 Webhook adresi gizli bir şifre değildir. Çağrının gerçekten sağlayıcıdan geldiğini ve yolda değişmediğini HMAC imzası kanıtlar.
  2. 2 Sağlayıcı cevabımızı alamazsa aynı olayı yeniden gönderir. Olay kimliği kaydedilir, daha önce işlenmiş olay atlanır.
  3. 3 Sıra önemlidir: önce imza, sonra kimlik. Doğrulanmamış bir çağrıya kimlik ayırmak, gerçeğini dışarıda bırakır.
Sonraki kapı Yalnızca bir alanın adını daha anlaşılır yaptın. Ertesi sabah müşterilere "0 TL havale geldi" SMS'i gidiyor. Nasıl? Olay Şemasının Evrimi — Alanın Adını Değiştirdin, Kim Kırıldı? · 8 dk

4 kart sonraki derste seni bekliyor

0/4 kart bu dersten toplandı