Webhook imzası doğrulama
Ödeme bildirimi, yalnızca mağazanızın ve hizmetimizin bildiği bir adrese gelir. Ancak adres sızabilir — günlüklerden, yapılandırmadan, geliştirme geçmişinden. Gerçek bildirimi sahtesinden ayırmak için her istek, webhook gizli anahtarıyla imzalanır.
Siparişi teslim etmeden veya durumunu değiştirmeden önce imzayı doğrulayın.
İmza başlıkları
| Başlık | Değer |
|---|---|
X-Timestamp | Gönderim zamanı, saniye cinsinden unix time |
X-Signature | sha256= öneki ve hex olarak HMAC-SHA256 |
İmzalanan dize, gönderim zamanı, bir nokta ve istek gövdesinden oluşur:
1756901234.{"wallet":{...},"project":{...},"invoice":{...},"payment":{...}}
İmzalama anahtarı, proje ayarlarındaki Webhook gizli anahtarı (Webhook secret) alanıdır. Değer ele geçirilirse orada yeniden verebilirsiniz.
Doğrulama adımları
- Ham istek gövdesini, JSON'u ayrıştırmadan önce alın.
X-Timestampdeğerini, bir noktayı ve gövdeyi birleştirin.- Webhook gizli anahtarıyla HMAC-SHA256'yı hesaplayın.
- Sonucu, sabit zamanlı karşılaştırma kullanarak
X-Signatureile karşılaştırın. X-Timestampşu anki zamandan çok sapıyorsa isteği reddedin.
Uygulama örnekleri
- PHP
- TypeScript
- JavaScript
- Python
<?php
$secret = 'webhook secret from your project settings';
$body = file_get_contents('php://input');
$timestamp = $_SERVER['HTTP_X_TIMESTAMP'] ?? '';
$signature = $_SERVER['HTTP_X_SIGNATURE'] ?? '';
// Guards against a replayed request
if (abs(time() - (int)$timestamp) > 300) {
http_response_code(400);
exit;
}
$expected = 'sha256='.hash_hmac('sha256', $timestamp.'.'.$body, $secret);
// hash_equals compares in constant time
if (!hash_equals($expected, $signature)) {
http_response_code(403);
exit;
}
$data = json_decode($body, true);
// Handle the order
http_response_code(200);
import { createHmac, timingSafeEqual } from 'node:crypto';
import express, { type Request, type Response } from 'express';
const app = express();
const secret = 'webhook secret from your project settings';
// The body must stay raw, so express.raw instead of express.json
app.post('/webhook', express.raw({ type: 'application/json' }), (req: Request, res: Response) => {
const timestamp = req.get('X-Timestamp') ?? '';
const signature = req.get('X-Signature') ?? '';
const body = (req.body as Buffer).toString('utf8');
if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) {
return res.sendStatus(400);
}
const expected = 'sha256=' + createHmac('sha256', secret)
.update(`${timestamp}.${body}`, 'utf8')
.digest('hex');
const a = Buffer.from(expected);
const b = Buffer.from(signature);
// Check the length first: timingSafeEqual needs buffers of equal size
if (a.length !== b.length || !timingSafeEqual(a, b)) {
return res.sendStatus(403);
}
const data = JSON.parse(body);
// Handle the order
res.sendStatus(200);
});
Aynı işleyicinin türsüz hâli — düz bir JavaScript projesi için.
import { createHmac, timingSafeEqual } from 'node:crypto';
import express from 'express';
const app = express();
const secret = 'webhook secret from your project settings';
// The body must stay raw, so express.raw instead of express.json
app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
const timestamp = req.get('X-Timestamp') ?? '';
const signature = req.get('X-Signature') ?? '';
const body = req.body.toString('utf8');
if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) {
return res.sendStatus(400);
}
const expected = 'sha256=' + createHmac('sha256', secret)
.update(`${timestamp}.${body}`, 'utf8')
.digest('hex');
const a = Buffer.from(expected);
const b = Buffer.from(signature);
// Check the length first: timingSafeEqual needs buffers of equal size
if (a.length !== b.length || !timingSafeEqual(a, b)) {
return res.sendStatus(403);
}
const data = JSON.parse(body);
// Handle the order
res.sendStatus(200);
});
import hmac
import hashlib
import time
from flask import Flask, request
app = Flask(__name__)
secret = 'webhook secret from your project settings'
@app.post('/webhook')
def webhook():
timestamp = request.headers.get('X-Timestamp', '')
signature = request.headers.get('X-Signature', '')
body = request.get_data(as_text=True)
if abs(time.time() - int(timestamp or 0)) > 300:
return '', 400
expected = 'sha256=' + hmac.new(
secret.encode(),
f'{timestamp}.{body}'.encode(),
hashlib.sha256,
).hexdigest()
if not hmac.compare_digest(expected, signature):
return '', 403
data = request.get_json()
# Handle the order
return '', 200
Akılda tutulması gerekenler
Ham gövde imzalanır. JSON'u ayrıştırıp yeniden oluşturursanız anahtar sırası ve boşluklar değişir — imza eşleşmez. HMAC'i ayrıştırmadan önce hesaplayın.
Her yeniden denemenin kendi imzası vardır. Gönderim zamanı her yeniden denemede yenidir, dolayısıyla imza da farklıdır. Gelecekteki isteklerle karşılaştırmak için imza saklamayın.
Gizli anahtar yeniden verilebilir. Eski gizli anahtar, yeniden verildikten hemen sonra çalışmayı durdurur; bu yüzden mağazanızdaki değeri aynı anda güncelleyin.
İmza doğrulama, idempotentliğin yerini tutmaz. İmza, isteğin gerçek olduğunu kanıtlar; onu ilk kez gördüğünüzü değil. Yeniden deneme, geçerli bir imzayla gelir — invoice.id üzerinden kontrol edin.