Қысқаша
Қолтаңба былай есептеледі: HMAC-SHA256(secret, timestamp + "." + rawBody), нәтижесі hex түрінде, sha256= префиксімен X-Webhook-Signature тақырыбында келеді. Сәйкес келмеуінің ең жиі себебі біреу: сіз денені JSON-ға айналдырып, сосын қайта жолға түрлендіресіз. Бұл кезде байттар өзгереді — бос орындар, өрістердің реті, Unicode таңбалары — сондықтан қолтаңба ешқашан сәйкес келмейді.
Шешім: денені өзгертілмеген байт күйінде алып, сол күйінде тексеру. Ал JSON-ға айналдыруды тексеруден кейін жасау.
Дұрыс есептеу
import crypto from 'node:crypto';
function verify(secret, timestamp, rawBody, signature) {
const expected = 'sha256=' + crypto
.createHmac('sha256', secret)
.update(timestamp + '.' + rawBody) // rawBody — Buffer немесе өзгермеген жол
.digest('hex');
const a = Buffer.from(expected);
const b = Buffer.from(signature || '');
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
Салыстыруды === арқылы емес, timingSafeEqual арқылы жасаңыз.
Тақырыптар:
| Тақырып | Не келеді |
|---|---|
X-Webhook-Event | Оқиға атауы, мысалы invoice.paid |
X-Webhook-Timestamp | Қолтаңбаға кіретін уақыт белгісі |
X-Webhook-Signature | sha256= префиксі бар hex қолтаңба |
X-Webhook-Delivery | Жеткізу идентификаторы, журналға жазуға ыңғайлы |
Симптом бойынша себеп
| Симптом | Себебі | Шешімі |
|---|---|---|
| Ешқашан сәйкес келмейді | Дене JSON-ға айналдырылып, қайта жолға түрлендірілген | Raw body алыңыз |
| Кейде сәйкес келеді, кейде жоқ | Денеде кириллица немесе эмодзи бар, қайта түрлендіру байтты өзгертеді | Raw body алыңыз |
| Барлық жерде сәйкес, тек продакшенде емес | Құпия орталары бойынша бөлек, .env жаңартылмаған | Сол орта үшін жасалған құпияны қойыңыз |
sha256= префиксі артық болып тұр | Салыстыруда префикс есепке алынбаған | Күтілетін мәнге де sha256= қосыңыз |
| Timestamp ескі деп қабылданбайды | Сервер сағаты ауытқыған немесе оқиға кезекте ұзақ тұрған | Сағатты NTP-ге қосыңыз |
| Қолтаңба API кілтпен есептелген | Құпия мен API кілт шатастырылған | Webhook құпиясын қолданыңыз |
Ең жиі қате: денені қайта құрастыру
Көп фреймворк келген JSON-ды автоматты түрде объектіге айналдырады. Кейін сіз сол объектіні қайта жолға түрлендірсеңіз, нәтиже түпнұсқаға байт-байт сәйкес келмейді:
// ДҰРЫС ЕМЕС — қолтаңба ешқашан сәйкес келмейді
const raw = JSON.stringify(req.body);
Неге:
- Бос орындар мен жол ауыстырулар жоғалады немесе қосылады
- Өрістердің реті өзгеруі мүмкін
- Сандар басқаша жазылады:
1000.0→1000 - Кириллица мен эмодзи басқа түрде экрандалады
- Бос объектілер мен
nullмәндер басқаша шығады
HMAC — байттардың функциясы. Бір ғана байт өзгерсе, қолтаңба мүлде басқа болады.
Raw body қалай алынады
Express. Қолтаңбаны тексеретін маршрутқа express.json() орнына express.raw() қойыңыз:
app.post('/webhooks/qutpay',
express.raw({ type: 'application/json' }),
(req, res) => {
const raw = req.body; // Buffer, өзгермеген
const ts = req.headers['x-webhook-timestamp'];
const sig = req.headers['x-webhook-signature'];
if (!verify(process.env.WEBHOOK_SECRET, ts, raw, sig)) {
return res.sendStatus(400);
}
const event = JSON.parse(raw.toString('utf8')); // тексергеннен КЕЙІН
res.sendStatus(200);
});
Маңызды: express.json() бүкіл қосымшаға app.use(express.json()) арқылы қосылып тұрса, ол бұл маршрутқа дейін жетіп үлгереді. Webhook маршрутын жалпы express.json() шақырылғанға дейін тіркеңіз.
Laravel.
$raw = $request->getContent(); // өзгермеген дене
$expected = 'sha256=' . hash_hmac(
'sha256',
$request->header('X-Webhook-Timestamp') . '.' . $raw,
config('services.qutpay.webhook_secret')
);
if (!hash_equals($expected, $request->header('X-Webhook-Signature', ''))) {
abort(400);
}
$event = json_decode($raw, true);
$request->all() емес, дәл $request->getContent() қолданыңыз.
Django.
import hmac, hashlib
raw = request.body # bytes, өзгермеген
ts = request.headers.get('X-Webhook-Timestamp', '')
sig = request.headers.get('X-Webhook-Signature', '')
expected = 'sha256=' + hmac.new(
secret.encode(),
ts.encode() + b'.' + raw,
hashlib.sha256,
).hexdigest()
if not hmac.compare_digest(expected, sig):
return HttpResponseBadRequest()
request.POST немесе request.data емес, request.body керек. DRF қолдансаңыз, request.body-ға request.data оқылғанға дейін қол жеткізіңіз.
Таза Node.
const chunks = [];
req.on('data', (c) => chunks.push(c));
req.on('end', () => {
const raw = Buffer.concat(chunks);
// тексеру осында
});
Timestamp тексеру
Қолтаңба дұрыс болса да, ескі сұрауды қабылдамаған жөн. X-Webhook-Timestamp мәні 5 минуттан ескі болмауы керек:
const age = Math.abs(Date.now() / 1000 - Number(ts));
if (age > 300) return res.sendStatus(400);
Бұл қайталап жіберу шабуылынан қорғайды. Егер бұл тексеру жалған түрде істеп жатса, серверіңіздің сағатын тексеріңіз: NTP қосулы болуы керек.
Құпияны шатастырмаңыз
Екі түрлі құпия бар және олар бір-біріне мүлде ұқсамайды:
| Не | Қайда қолданылады | Пішімі |
|---|---|---|
| API кілт | Сұрау жіберуде, X-API-Key тақырыбында | qp_live_… / qp_test_… |
| Webhook құпиясы | Тек келген webhook қолтаңбасын тексеруде | Бөлек мән, qp_ деп басталмайды |
Webhook құпиясы кабинетте бір рет қана көрсетіледі — webhook адресін қосқан сәтте. Жоғалтып алсаңыз, қайта қарауға болмайды: жаңасын жасап, кодыңызды жаңартасыз.
Егер X-API-Key тақырыбымен мәселе болса, ол мүлде басқа әңгіме: API 401 қайтарады.
Тексеру реті
- Келген сұраудың дәл байттарын журналға жазыңыз (hex немесе base64 түрінде)
- Сол байттардан қолтаңбаны қолмен есептеп, тақырыптағы мәнмен салыстырыңыз
- Сәйкес келсе — мәселе кодыңыздағы raw body алуда
- Сәйкес келмесе — құпия басқа немесе timestamp дұрыс қосылмаған
timestamp + "." + rawBodyтәртібін тексеріңіз: нүкте бөлгіш ретінде тұр- Бәрі дұрыс көрінсе: Мәселе менде ме, Kaspi-де ме
Жиі қойылатын сұрақтар
Қолтаңба сәйкес келмесе, қандай жауап беру керек? 400. Сонда оқиға қайталанады — бұл дұрыс: кодыңызды түзетіп үлгерсеңіз, оқиға жоғалмайды.
Тексермеуге бола ма? Болмайды. Webhook адресіңіз авторизациясыз ашық тұрады, яғни оған кез келген адам сұрау жібере алады. Қолтаңба — сұраудың бізден келгенінің жалғыз дәлелі.
Құпияны ауыстырсам, ескі webhook-тар не болады? Ауыстырған сәттен бастап жаңа құпия қолданылады. Кодты бір уақытта жаңартыңыз.
Бір адресте бірнеше құпия бола ма? Әр webhook адресінің өз құпиясы болады. Бірнеше адрес қоссаңыз, әрқайсысын бөлек сақтаңыз.
Прокси артындағы серверде істей ме? Иә, бірақ прокси денені өзгертпейтініне көз жеткізіңіз. Кейбір баптаулар JSON-ды қайта пішімдейді — ондайда қолтаңба ешқашан сәйкес келмейді.