Panduan Pasukan Lucee MY

Webhook tempatan dalam CFML: sahkan tandatangan, idempoten, cuba semula

Panduan webhook CFML: sahkan tandatangan HMAC-SHA256 dan MD5, banding tetap masa, idempotensi dengan jadual transaksi, balas 200 untuk hentikan cuba semula.

Webhook tempatan dalam CFML: sahkan tandatangan, idempoten, cuba semula
0
Kandungan

Webhook tempatan dalam CFML: sahkan tandatangan, idempoten, cuba semula

Webhook ialah panggilan balik HTTP yang dihantar pembekal ke pelayan anda apabila sesuatu berlaku, contohnya pembayaran selesai. Apabila anda menerima panggilan sedemikian, anda perlu menjawab empat soalan: adakah permintaan ini benar-benar daripada pembekal, adakah saya sudah memprosesnya, bagaimana saya elak memproses dua kali, dan bagaimana saya bertindak apabila panggilan berulang. Artikel ini mengajar empat perkara tersebut dengan kod CFML sebenar, dan membezakan dengan jelas antara keperluan pembekal dan amalan am.

Mengapa webhook perlu pengesahan dan idempotensi

Callback adalah titik masuk awam. Sesiapa yang tahu URL anda boleh menghantar POST dengan data palsu, jadi anda mesti mengesahkan setiap panggilan sebelum menyentuh pangkalan data. Pada masa yang sama, pembekal boleh menghantar panggilan yang sama berulang kali apabila endpoint anda lambat membalas, jadi pemprosesan anda mesti idempoten: memproses peristiwa yang sama dua kali mesti memberikan hasil yang sama seperti sekali.

Dua sifat ini saling melengkapi. Pengesahan memastikan data datang daripada sumber yang betul; idempotensi memastikan data yang sah tetapi berulang tidak merosakkan pesanan anda. Corak yang dibentangkan di sini diambil daripada gerbang pembayaran Malaysia yang sebenar, tetapi ia terpakai kepada mana-mana webhook.

Membaca badan permintaan dan membezakan panggilan yang sah

Kebanyakan gerbang menghantar callback sebagai POST borang, jadi parameter tersedia dalam skop form. Sebilangan lain menghantar JSON mentah, yang perlu anda baca daripada badan permintaan. Gunakan getHttpRequestData() untuk melihat badan mentah tanpa mengira formatnya.

req = getHttpRequestData();

if (isJSON(req.content)) {
    payload = deserializeJSON(req.content);
} else {
    payload = form;
}

if (!structKeyExists(payload, "order_id") || !structKeyExists(payload, "hash")) {
    cfheader(statuscode = 400);
    cfcontent(reset = true);
    abort;
}

Membezakan panggilan yang sah bermaksud dua langkah: medan yang anda jangkakan wujud, dan tandatangan sepadan. Kehadiran medan sahaja tidak mencukupi, kerana penyerang boleh menghantar medan itu juga. Tandatangan ialah perkara yang benar-benar memisahkan panggilan pembekal daripada panggilan palsu.

Tandatangan tiga gerbang: MD5 lawan HMAC-SHA256

Gerbang Malaysia menggunakan pola tandatangan yang berbeza, dan anda mesti mengikut formula pembekal masing-masing. ToyyibPay menggunakan MD5 mudah: hash = MD5(userSecretKey + status + order_id + refno + "ok"). senangPay menggunakan md5(secretKey + status_id + order_id + transaction_id + msg) atau HMAC-SHA256. Billplz menggunakan HMAC-SHA256 yang paling ketat, dengan kekunci disusun menaik dan dicantum dengan paip.

Perbezaan penting ialah apa yang melindungi tandatangan. MD5 menggabungkan rahsia terus ke dalam rentetan, manakala HMAC-SHA256 menggunakan rahsia sebagai kunci hash. Kedua-duanya sah untuk gerbang masing-masing; jangan tukar algoritma sesuka hati. Hash MD5 tidak mengandungi masa atau nonce, jadi ia hanya melindungi integriti, bukan anti-replay; itu diserahkan kepada amalan am seperti menyimpan ID transaksi yang sudah diproses.

Sahkan HMAC-SHA256 gaya Billplz dalam CFML

Billplz membina x_signature dengan mengambil semua pasangan key-value kecuali x_signature sendiri, menggabung setiap key dengan value tanpa pemisah, menyusunnya menaik tanpa sensitif huruf besar atau kecil, mencantum dengan |, kemudian mengira HMAC-SHA256(source, XSignatureKey).

kekunci = structKeyArray(form);
arrayDelete(kekunci, "x_signature");

gabungan = [];
for (k in kekunci) {
    arrayAppend(gabungan, k & form[k]);
}
arraySort(gabungan, "textnocase");
src = arrayToList(gabungan, "|");

dikira = hmac(src, xSignatureKey, "HmacSHA256");
sah = samaMalar(lcase(dikira), lcase(form.x_signature));

Ini ialah contoh terbaik untuk dijadikan templat, kerana pengisihan kekunci memastikan susunan parameter yang tidak menentu tidak mematahkan pengesahan. Untuk Billplz V5 (Payment Order), tandatangannya ialah HMAC-SHA512 dengan nilai dalam urutan tetap seperti [id, bank_account_number, status, total, reference_id, epoch], jadi anda mencantum nilai tersebut dalam urutan itu tanpa pengisihan.

Sahkan hash MD5 gaya ToyyibPay dan senangPay dalam CFML

ToyyibPay menambah pemalar "ok" pada hujung rentetan sebelum MD5, manakala senangPay tidak. Perbezaan kecil ini menunjukkan kenapa anda mesti menyalin formula pembekal tepat seperti yang didokumenkan.

// ToyyibPay: MD5(userSecretKey + status + order_id + refno + "ok")
dikiraToyyib = lcase(hash(
    userSecretKey & form.status & form.order_id & form.refno & "ok",
    "md5"
));
sahToyyib = samaMalar(lcase(dikiraToyyib), lcase(form.hash));

// senangPay: md5(secretKey + status_id + order_id + transaction_id + msg)
dikiraSenang = lcase(hash(
    secretKey & form.status_id & form.order_id & form.transaction_id & form.msg,
    "md5"
));
sahSenang = samaMalar(lcase(dikiraSenang), lcase(form.hash));

Untuk senangPay, nilai msg yang diterima mungkin mengandungi garis bawah; gunakan nilai mentah yang diterima dalam hash, jangan ganti garis bawah dahulu. Formula penuh untuk setiap gerbang dirujuk dalam panduan sambung senangPay dan artikel gerbang yang lain.

Perbandingan tetap masa

Perbandingan hash biasa dengan == berhenti sebaik sahaja aksara berbeza, jadi masa yang diambil mendedahkan berapa banyak aksara awal yang betul. Penyerang yang mengukur masa ini boleh meneka tandatangan secara beransur-ansur. Perbandingan tetap masa mengelakkan kebocoran itu dengan sentiasa memeriksa keseluruhan rentetan.

function samaMalar(required string a, required string b) {
    if (len(a) != len(b)) {
        return false;
    }
    var bez = 0;
    for (var i = 1; i <= len(a); i++) {
        if (asc(mid(a, i, 1)) != asc(mid(b, i, 1))) {
            bez++;
        }
    }
    return bez == 0;
}

Fungsi ini tidak keluar awal, jadi masa larian tidak bergantung pada kedudukan aksara yang berbeza. Ini ialah amalan am, bukan keperluan bertulis mana-mana pembekal, tetapi ia kos rendah dan patut menjadi tabiat. Sebagai alternatif, anda boleh memanfaatkan java.security.MessageDigest.isEqual() melalui createObject("java", "java.security.MessageDigest"), tetapi versi CFML tulen di atas mudah dibaca dan mudah alih.

Idempotensi dengan jadual transaksi

Oleh sebab panggilan boleh berulang, simpan ID transaksi dalam jadual dan langkau pemprosesan jika ia sudah wujud. Kunci unik ialah ID transaksi atau gabungan ID pesanan dengan status pembayaran.

sudah = queryExecute(
    "SELECT 1 AS ada FROM webhook_transaksi WHERE transaction_id = :tid",
    { tid = { value = form.transaction_id, cfsqltype = "cf_sql_varchar" } },
    { datasource = "kedai" }
);

if (sudah.recordCount > 0) {
    cfheader(statuscode = 200);
    cfcontent(reset = true);
    abort;
}

queryExecute(
    "INSERT INTO webhook_transaksi (transaction_id, order_id, status_id)
     VALUES (:tid, :oid, :status)",
    {
        tid = { value = form.transaction_id, cfsqltype = "cf_sql_varchar" },
        oid = { value = form.order_id, cfsqltype = "cf_sql_varchar" },
        status = { value = form.status_id, cfsqltype = "cf_sql_varchar" }
    },
    { datasource = "kedai" }
);

Parameterisasi nilai dengan cfsqltype mengikut panduan cfquery dan cfqueryparam. Corak semak-sebelum-insert di atas mudah dibaca, tetapi dalam persekitaran serentak dua panggilan boleh melepasi semakan serentak. Untuk perlindungan penuh, tambah kekangan unik pada transaction_id dan tangkap ralat insert kedua, atau gunakan UPSERT yang disokong pangkalan data anda.

Cuba semula: balas 200 segera dan proses tak segerak

Sesetengah pembekal menetapkan jadual cuba semula yang jelas. Billplz mengulang callback sehingga lima kali jika endpoint tidak membalas HTTP 200 dalam masa 20 saat, kemudian membuangnya daripada giliran. Untuk ToyyibPay dan senangPay, jadual cuba semula untuk pembayaran biasa tidak dinyatakan dalam dokumen rasmi. Ini ialah keperluan pembekal yang perlu anda hormati: balas 200 secepat mungkin.

// Sahkan tandatangan dahulu, kemudian akui dengan segera
cfheader(statuscode = 200, statustext = "OK");
cfcontent(type = "text/plain", reset = true);

Amalan am yang menyokongnya ialah jangan memproses kerja berat dalam permintaan callback. Hantar 200 serta-merta, kemudian alihkan kerja seperti menghantar emel atau mengemas kini laporan ke baris gilir atau utas berasingan. Dengan cara itu, walaupun pemprosesan sebenar lambat, pengakuan pantas menghalang pembekal daripada menghantar panggilan tambahan. Terima kemungkinan panggilan berulang sebagai keadaan biasa, bukan ralat: dedupe dengan jadual transaksi menjadikan panggilan kedua tidak berbahaya.

Webhook tempatan: tunnel dan log mentah

Semasa pembangunan anda selalunya bekerja pada localhost, tetapi pembekal tidak dapat menghubungi mesin tempatan anda. ToyyibPay secara bertulis menyatakan callback tidak boleh localhost. Amalan am ialah menggunakan tunnel seperti ngrok atau cloudflared untuk mendedahkan pelayan tempatan anda ke internet buat sementara waktu, dan meletakkan URL tunnel sebagai Callback URL semasa ujian.

Simpan badan mentah dan pengepala setiap callback untuk audit dan nyahpepijat. Ini penting kerana apabila pengesahan gagal, anda perlu melihat apa yang sebenarnya dihantar.

req = getHttpRequestData();
writeLog(text = req.content, file = "webhook");

Log ini ialah amalan am, bukan keperluan pembekal, tetapi ia menjimatkan masa apabila sesuatu tidak sepadan. Untuk perbincangan lanjut tentang membaca permintaan luar dan pemboleh ubah persekitaran, rujuk panduan cfhttp dan panggilan API luar dan panduan rahsia dan pemboleh ubah persekitaran.

Senarai semak keselamatan

Sebelum menutup topik ini, jalankan senarai berikut untuk setiap endpoint webhook anda. Sahkan tandatangan sebelum menyentuh data; gunakan perbandingan tetap masa; simpan kunci unik transaksi untuk idempotensi; balas 200 dengan segera; semak semula status pada pembekal sebelum menandakan pesanan selesai; simpan ID transaksi yang sudah diproses untuk menolak duplikat; dan log badan mentah untuk audit. Tandatangan sah tidak bermakna permintaan itu baharu, jadi anti-replay melalui dedupe adalah penting untuk hash MD5 yang tidak mengandungi masa.

Soalan Lazim (FAQ)

Mengapa callback saya diproses dua kali?

Kerana pembekal seperti Billplz mengulang callback jika anda lambat membalas 200. Balas 200 segera dan dedupe dengan kunci unik transaksi supaya panggilan kedua dilangkau.

Apakah perbezaan MD5 dan HMAC-SHA256?

MD5 menggabungkan rahsia terus ke dalam rentetan yang di-hash, manakala HMAC-SHA256 menggunakan rahsia sebagai kunci hash. Kedua-duanya sah untuk gerbang masing-masing; ikut formula pembekal dan jangan tukar algoritma.

Perlukah saya sahkan setiap callback?

Ya. Sahkan tandatangan sebelum memproses apa-apa. Tanpa itu, sesiapa yang tahu URL anda boleh menghantar POST dengan status bayaran palsu.

Bolehkah saya menguji webhook di localhost?

Tidak terus, kerana pembekal tidak dapat menghubungi mesin tempatan anda, dan ToyyibPay menyatakan callback tidak boleh localhost. Guna tunnel seperti ngrok atau cloudflared semasa pembangunan.

Apa maksud amalan am dalam artikel ini?

Langkah yang baik untuk semua integrasi, seperti idempotensi, balas 200 segera, perbandingan tetap masa dan semak semula status, tetapi tidak diwajibkan secara bertulis oleh pembekal.

Sumber

0 komen

Log masuk untuk menjawab atau memberi komen.

Log masuk