Qut Pay Сайт Кабинет Білім базасы Нұсқаулықтар API құжаттамасы ҚАЗРУС
Басты бетБілім базасы → Анықтамалық

Webhook қауіпсіздігі және қолтаңбаны тексеру

Жаңартылды: 2026-09-14 · Markdown нұсқасы

Қысқаша

Webhook адресі интернетте ашық тұрады, сондықтан кез келген адам сізге «счёт төленді» деген сұрау жібере алады. Одан қорғайтын жалғыз нәрсе — қолтаңбаны тексеру.

Қолтаңба былай құралады:

signature = "sha256=" + hex( HMAC-SHA256( secret, timestamp + "." + rawBody ) )

Мұндағы timestampX-Webhook-Timestamp тақырыбындағы мән, rawBody — сұраудың денесі өзгертілмеген байт күйінде. Нәтижені X-Webhook-Signature тақырыбындағы мәнмен салыстырасыз.

Үш тексеру міндетті: қолтаңба сәйкес келсін, timestamp 5 минуттан ескі болмасын, өңдеу идемпотентті болсын.

Raw body неге міндетті

Ең жиі кездесетін қате осы. Фреймворктердің көбі денені автоматты түрде JSON-ға айналдырады, ал сіз оны қайтадан жолға айналдырғанда байттар өзгереді: бос орындар, өрістердің реті, Unicode таңбаларының жазылуы.

HMAC байт бойынша есептеледі. Бір байт өзгерсе, қолтаңба мүлдем басқа шығады.

Қате тәсілДұрыс тәсіл
JSON.stringify(req.body)req.body — Buffer күйінде
json.dumps(request.json)request.body немесе request.get_data()
json_encode($data)file_get_contents('php://input')

Ереже: денені JSON-ға айналдырғанға дейін қолтаңбаны тексеріңіз, содан кейін ғана талдаңыз.

Timestamp тексеру

X-Webhook-Timestamp — жіберілген уақыт, Unix секунд. Ол 5 минуттан ескі болса, сұрауды қабылдамаңыз.

Бұл қайта жіберу шабуылынан қорғайды: біреу бұрын жарамды болған сұрауды ұстап алып, кейін қайта жіберсе, қолтаңбасы дұрыс болғанымен уақыты ескіріп қалады.

Серверіңіздің сағаты дұрыс жүріп тұрғанына көз жеткізіңіз — сағат бірнеше минутқа ауытқыса, жарамды сұраулар да қабылданбай қалады.

Express (Node.js)

Маңыздысы — express.json() осы маршрутқа дейін қолданылмауы керек.

import crypto from 'node:crypto';
import express from 'express';

const app = express();

function verify(req) {
  const ts = req.get('X-Webhook-Timestamp') || '';
  const got = req.get('X-Webhook-Signature') || '';
  if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false;

  const mac = crypto
    .createHmac('sha256', process.env.QUTPAY_WEBHOOK_SECRET)
    .update(ts + '.')
    .update(req.body)          // Buffer, өзгертілмеген
    .digest('hex');
  const expected = 'sha256=' + mac;

  const a = Buffer.from(expected);
  const b = Buffer.from(got);
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

app.post('/qutpay-webhook', express.raw({ type: '*/*' }), (req, res) => {
  if (!verify(req)) return res.sendStatus(401);
  res.sendStatus(200);
  const { event, invoice } = JSON.parse(req.body.toString('utf8'));
  handle(event, invoice);
});

Салыстыруда қарапайым теңдіктің орнына timingSafeEqual қолданыңыз: әдеттегі салыстыру қолтаңбаны әріптеп табуға жол ашады.

Laravel (PHP)

Laravel денені де оқып қояды, бірақ $request->getContent() бастапқы байттарды береді. Бұл маршрутты CSRF тексеруінен шығарыңыз.

public function handle(Request $request)
{
    $raw = $request->getContent();
    $ts  = $request->header('X-Webhook-Timestamp', '');
    $got = $request->header('X-Webhook-Signature', '');

    if (abs(time() - (int) $ts) > 300) {
        return response('stale', 401);
    }

    $mac = hash_hmac('sha256', $ts . '.' . $raw, config('services.qutpay.webhook_secret'));
    if (!hash_equals('sha256=' . $mac, $got)) {
        return response('bad signature', 401);
    }

    $payload = json_decode($raw, true);
    dispatch(new HandleQutPayEvent($payload));   // жұмысты кезекке

    return response('', 200);
}

Таза PHP-де дене file_get_contents('php://input') арқылы алынады, тақырыптар — getallheaders().

Django (Python)

import hmac, hashlib, time, json
from django.conf import settings
from django.http import HttpResponse
from django.views.decorators.csrf import csrf_exempt

@csrf_exempt
def qutpay_webhook(request):
    raw = request.body                      # bytes, өзгертілмеген
    ts = request.headers.get("X-Webhook-Timestamp", "")
    got = request.headers.get("X-Webhook-Signature", "")

    if abs(time.time() - int(ts or 0)) > 300:
        return HttpResponse("stale", status=401)

    mac = hmac.new(
        settings.QUTPAY_WEBHOOK_SECRET.encode(),
        ts.encode() + b"." + raw,
        hashlib.sha256,
    ).hexdigest()

    if not hmac.compare_digest("sha256=" + mac, got):
        return HttpResponse("bad signature", status=401)

    payload = json.loads(raw)
    handle(payload)
    return HttpResponse(status=200)

Flask-та дене request.get_data() арқылы алынады, қалғаны бірдей.

Фреймворксіз Node

import http from 'node:http';

http.createServer((req, res) => {
  const chunks = [];
  req.on('data', (c) => chunks.push(c));
  req.on('end', () => {
    const raw = Buffer.concat(chunks);     // өзгертілмеген байттар
    if (!verifyRaw(raw, req.headers)) {
      res.writeHead(401).end();
      return;
    }
    res.writeHead(200).end();
    handle(JSON.parse(raw.toString('utf8')));
  });
}).listen(3000);

Идемпотентті өңдеу

Қолтаңба дұрыс болса да, бір оқиға бірнеше рет келуі мүмкін: желі үзілді, сіз 200 қайтара алмадыңыз, біз қайталадық. Жалпы қайталау саны — 11 рет.

Сондықтан өңдеуді (invoice.id, status) жұбы бойынша бір рет орындаңыз. Ең қарапайым тәсіл — сол жұпты өз базаңызға бірегей кілтпен жазу:

CREATE TABLE qutpay_events (
  invoice_id TEXT NOT NULL,
  status     TEXT NOT NULL,
  handled_at TIMESTAMPTZ DEFAULT now(),
  PRIMARY KEY (invoice_id, status)
);
const ins = await db.query(
  'INSERT INTO qutpay_events (invoice_id, status) VALUES ($1, $2) ON CONFLICT DO NOTHING',
  [invoice.id, invoice.status],
);
if (ins.rowCount === 0) return;   // бұрын өңделген
await fulfil(invoice);

Толығырақ: Идемпоттылық.

Жиі кездесетін қателер

Не істелдіНәтижесіШешімі
Дене JSON-ға айналдырылған соң қайта жолға айналдырылдыҚолтаңба ешқашан сәйкес келмейдіRaw body қолданыңыз
timestamp қолтаңбаға қосылмағанҚолтаңба сәйкес келмейдіtimestamp + "." + rawBody реті сақталсын
sha256= префиксі салыстыруда ескерілмегенҚолтаңба сәйкес келмейдіПрефиксті қосып салыстырыңыз немесе екеуінен де алып тастаңыз
Құпия қате көшірілген, соңында бос орын барҚолтаңба сәйкес келмейдіҚұпияны қайта көшіріңіз
Сервер сағаты ауытқығанЖарамды сұраулар «ескі» болып қабылданбайдыУақытты синхрондаңыз
Қолтаңба мүлдем тексерілмейдіКез келген адам жалған «төленді» жібере аладыТексеруді қосыңыз
Адрес авторизациямен жабылғанWebhook мүлдем келмейдіАдресті ашық қалдырып, қолтаңбамен қорғаңыз

Қолтаңба сәйкес келмей жатса: Webhook қолтаңбасы сәйкес келмейді.

Қосымша шаралар

Жиі қойылатын сұрақтар

Қолтаңбаны тексермей-ақ қойсам бола ма? Болмайды. Ондай адреске кез келген адам «счёт төленді» деп жіберіп, тауарыңызды тегін алып кете алады.

IP бойынша сүзсем жеткілікті ме? Жоқ. IP адрестер өзгереді, ал қолтаңба деректің өзін дәлелдейді.

Құпия қашан ауысады? Сіз өзіңіз ауыстырғанда ғана. Жаңа құпия жасалған сәттен бастап қолданылады, серверіңізді бір мезетте жаңартыңыз.

Неге денеде төлемнің дәл уақыты жоқ? Kaspi төлем уақытын бермейді, сондықтан біз тек өзіміз білген сәтті бере аламыз.

Тексеруді қалай сынаймын? Кабинеттің Интеграциялар бөліміндегі сынау батырмасымен, сосын sandbox-та төлемді симуляциялап көріңіз.

Байланысты мақалалар

Webhook баптауКабинетте webhook адресін қосу, оқиғаларды таңдау, құпияны сақтау, тақырыптар мен дене пішімі, 11 рет қайталау кестесі, журналды оқу, адресті сынау және қайта бағыттау ережесі.Webhook қолтаңбасы сәйкес келмейдіҚолтаңба HMAC-SHA256(secret, timestamp + "." + rawBody) арқылы есептеледі. Ең жиі қате — денені JSON-ға айналдырып, қайта жолға түрлендіру. Raw body алудың Express, Laravel, Django мысалдары.Интеграция қауіпсіздігі: 12 тармақQut Pay интеграциясын қауіпсіз құрудың 12 нақты тармағы: кілтті қайда сақтау, scope-ты қалай шектеу, webhook қолтаңбасын қалай тексеру, журналға нені жазуға болмайды, кілт сыртқа шықса не істеу керек.Идемпоттылық: қайталаудан қорғануIdempotency-Key тақырыбы қалай жұмыс істейді, кілтті қалай құру керек, externalOrderId-дің рөлі неде, webhook өңдеуде және қайтаруда қайталанудан қалай сақтану керек.

Сұрағыңыз қалды ма? WhatsApp +77788813333 · kazprose@gmail.com
Кабинеттен де жазуға болады: Қолдау.

Qut Pay — тәуелсіз сервис, «Kaspi Bank» АҚ-мен аффилирленбеген. Kaspi және Kaspi Pay — құқық иесінің тауар белгілері.