Chuyển tới nội dung chính

Xác minh IPN

Mọi lời gọi IPN đều có chữ ký, để endpoint của bạn phân biệt được lời gọi thật từ Hạ Tầng với lời gọi giả mạo. Trang này giải thích chữ ký, đưa ra handler chạy được bằng bốn ngôn ngữ, một test vector để kiểm tra code, và những lỗi hay làm hỏng việc xác minh.

Các khoá​

Mỗi doanh nghiệp có một access key và một secret key cho mỗi môi trường, được tạo cùng doanh nghiệp. Xem trên dashboard tại Cài đặt thanh toán → Khoá tích hợp. Key development bắt đầu bằng ak_test_ và sk_test_.

  • Hatang-Access-Key cho biết lời gọi thuộc tài khoản và môi trường nào.
  • Hatang-Signature là HMAC-SHA256 dạng hex thường của body gốc, khóa là secret key (dạng byte UTF-8, đúng như hiển thị).

Để xác minh: tính HMAC trên đúng các byte nhận được, rồi so với header bằng hàm so sánh thời gian hằng. Chỉ giữ secret key ở server.

Ví dụ handler​

import express from "express";
import { createHmac, timingSafeEqual } from "node:crypto";

const SECRET = process.env.HATANG_SECRET_KEY; // sk_…

function verifyIpn(rawBody, signature, secret) {
const expected = createHmac("sha256", secret).update(rawBody).digest("hex");
const got = String(signature ?? "").trim().toLowerCase();
return got.length === expected.length && timingSafeEqual(Buffer.from(got), Buffer.from(expected));
}

const app = express();
// Byte gốc, không phải JSON đã parse: serialize lại sẽ đổi body và làm sai chữ ký.
app.post("/hatang/ipn", express.raw({ type: "application/json" }), async (req, res) => {
if (!verifyIpn(req.body, req.get("Hatang-Signature"), SECRET)) return res.status(401).end();
const ipn = JSON.parse(req.body.toString("utf8"));
if (await alreadyHandled(ipn.id)) return res.status(200).end();
await queue.add("hatang-ipn", ipn); // cập nhật đơn ở nền
res.status(200).end();
});

Với SDK: constructIpnEvent(rawBody, headers, secretKey) xác minh và trả về lời gọi đã parse (hoặc throw HatangSignatureError), còn verifyIpn(rawBody, signature, secretKey) trả về boolean. Xem SDK JavaScript.

Test vector​

Với secret key sk_test_example và body {"id":"ipn_test","type":"payment.succeeded"} (đúng các byte đó), chữ ký là:

35c86d268e66be5f34c25d834de9da3ba1f0c6d26fb735266440886df6b5d955

Kiểm tra bằng shell:

printf '%s' '{"id":"ipn_test","type":"payment.succeeded"}' | openssl dgst -sha256 -hmac sk_test_example

Và gửi một lời gọi test có chữ ký tới handler trên máy bạn:

BODY='{"id":"ipn_test","type":"payment.succeeded"}'
SIG=$(printf '%s' "$BODY" | openssl dgst -sha256 -hmac sk_test_example | awk '{print $NF}')
curl -i http://localhost:3000/hatang/ipn \
-H "Content-Type: application/json" \
-H "Hatang-Signature: $SIG" \
--data-raw "$BODY"

Với HATANG_SECRET_KEY=sk_test_example, handler phải trả 200; đổi một ký tự trong body thì phải trả 401. Để test trọn luồng, tạo payment ở development và giả lập chuyển khoản.

Lỗi thường gặp​

  • Parse JSON rồi serialize lại trước khi xác minh. Khoảng trắng và thứ tự key thay đổi, HMAC không khớp. Hãy xác minh trên byte gốc. Với Express, dùng express.raw() cho route này; với Next.js, đọc await req.text().
  • Dùng secret key của môi trường kia. Development và production có key khác nhau. Hatang-Environment và tiền tố sk_test_ cho biết key nào áp dụng.
  • So sánh bằng ==. Hãy dùng hàm so sánh thời gian hằng.
  • Proxy làm thay đổi body. Một số gateway mã hóa lại JSON hoặc đổi bảng mã. Hãy xác minh trên body đúng như Hạ Tầng gửi.
  • Xoay secret key. Sau khi bấm Tạo secret key mới trên dashboard, lời gọi được ký bằng key mới ngay lập tức. Các lần thử lại của lời gọi cũ được ký bằng key hiện hành lúc gửi, nên hãy đổi key trên server ngay sau khi xoay.

Bước tiếp theo​

Xem trang này dạng Markdown