Developer

Xác thực chữ ký (Signature)

✧ Mở bằng AI

Sao chép tài liệu, mở AI rồi dán vào cuộc trò chuyện.

ChatGPTClaudePerplexityGrok
≡ Xem Markdown

Collection Link sử dụng HMAC-SHA256 với secretKey để xác thực chuỗi dữ liệu đã ký. Kết quả là chuỗi hex viết thường, dài 64 ký tự.

HMAC không mã hóa nội dung: dữ liệu vẫn đọc được. Chữ ký chỉ bảo vệ các trường nằm trong chuỗi ký, không tự động bảo vệ toàn bộ JSON. Gửi request qua HTTPS và giữ secretKey ở backend.

Chọn đúng chuỗi ký

Thao tác Trường chữ ký Hướng xử lý
Tạo đơn V1 signature Merchant ký request gửi Pay2S
Tạo đơn V2 signature Ký thêm extraData và signatureVersion=2
Hủy đơn signature Ký riêng với requestType=cancel
Mô phỏng thanh toán demo signature Ký riêng với requestType=simulatePayment
Nhận IPN m2signature Merchant tính lại rồi so sánh

Không sắp xếp và ký tất cả khóa JSON. Dùng đúng danh sách và thứ tự trường của từng thao tác bên dưới. Các API dùng Bearer token hoặc pay2s-token có cơ chế xác thực riêng.

Quy tắc chung

  1. Lấy accessKey, partnerCode, secretKey từ cùng bộ khóa của tài khoản hoặc cửa hàng.
  2. Ghép tênTrường=giáTrị bằng dấu &, đúng thứ tự.
  3. Tính HMAC-SHA256 trên chuỗi UTF-8 với secretKey, xuất hex viết thường.
  4. Gửi kết quả trong trường chữ ký. Không gửi secretKey trong payload.

Tạo đơn V1

V1 cũ không đưa extraData và signatureVersion vào chuỗi ký:

accessKey={accessKey}&amount={amount}&bankAccounts=Array&ipnUrl={ipnUrl}&orderId={orderId}&orderInfo={orderInfo}&partnerCode={partnerCode}&redirectUrl={redirectUrl}&requestId={requestId}&requestType={requestType}

Payload có thể bỏ signatureVersion hoặc gửi "1". Không được bỏ signature. Không cần thêm extraData cho luồng V1 cũ.

Tạo đơn V2

Gửi signatureVersion: "2" và extraData không rỗng trong payload:

accessKey={accessKey}&amount={amount}&bankAccounts=Array&extraData={extraData}&ipnUrl={ipnUrl}&orderId={orderId}&orderInfo={orderInfo}&partnerCode={partnerCode}&redirectUrl={redirectUrl}&requestId={requestId}&requestType={requestType}&signatureVersion=2

extraData = Base64(JSON UTF-8 của metadata). Tạo giá trị này một lần, dùng cùng chuỗi để ký và gửi; không decode/re-encode sau khi ký. Xem schema tại Collection Link V2.

Node.js: tạo chữ ký V1/V2

import { createHmac } from 'node:crypto';

function signCreate(p, secretKey) {
  const version = String(p.signatureVersion ?? '1');
  if (!['1', '2'].includes(version)) throw new Error('Unsupported version');
  if (version === '2' && !p.extraData) throw new Error('V2 requires extraData');
  let raw = `accessKey=${p.accessKey}&amount=${p.amount}&bankAccounts=Array`;
  if (version === '2') raw += `&extraData=${p.extraData}`;
  for (const field of ['ipnUrl', 'orderId', 'orderInfo', 'partnerCode', 'redirectUrl', 'requestId', 'requestType']) {
    raw += `&${field}=${p[field]}`;
  }
  if (version === '2') raw += '&signatureVersion=2';
  return createHmac('sha256', secretKey).update(raw, 'utf8').digest('hex');
}

// payload là body create; metadata theo schema Collection Link V2.
// Với V2, thực hiện trước khi ký:
// payload.signatureVersion = '2';
// payload.extraData = Buffer.from(JSON.stringify(metadata), 'utf8').toString('base64');
// payload.signature = signCreate(payload, process.env.PAY2S_SECRET_KEY);
// Gửi chính payload này, không sửa các trường đã ký.

PHP: tạo chữ ký V1/V2

function signCreate(array $p, string $secretKey): string
{
    $version = (string)($p['signatureVersion'] ?? '1');
    if (!in_array($version, ['1', '2'], true)) {
        throw new InvalidArgumentException('Unsupported version');
    }
    if ($version === '2' && empty($p['extraData'])) {
        throw new InvalidArgumentException('V2 requires extraData');
    }
    $raw = 'accessKey=' . $p['accessKey'] . '&amount=' . $p['amount'] . '&bankAccounts=Array';
    if ($version === '2') $raw .= '&extraData=' . $p['extraData'];
    foreach (['ipnUrl', 'orderId', 'orderInfo', 'partnerCode', 'redirectUrl', 'requestId', 'requestType'] as $field) {
        $raw .= '&' . $field . '=' . $p[$field];
    }
    if ($version === '2') $raw .= '&signatureVersion=2';
    return hash_hmac('sha256', $raw, $secretKey);
}

// $payload['signatureVersion'] = '2';
// $payload['extraData'] = base64_encode(json_encode($metadata, JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR));
// $payload['signature'] = signCreate($payload, getenv('PAY2S_SECRET_KEY'));

Hai hàm trên dùng chuỗi V1 cũ cho version 1 và chuỗi đầy đủ cho version 2. Có thể dùng SDK Node.js/PHP để tạo request V2 và xác thực IPN.

Hủy đơn hàng

Hủy đơn dùng cùng bộ khóa của create nhưng có chuỗi ký riêng:

accessKey={accessKey}&orderId={orderId}&partnerCode={partnerCode}&requestId={requestId}&requestType=cancel

orderId là mã gửi lúc tạo đơn; requestId là mã yêu cầu hủy. Không thêm amount, extraData, signatureVersion hoặc tái sử dụng chữ ký create.

Xem API hủy đơn hàng để lấy payload, script Postman và quy tắc xử lý đơn đã thanh toán.

Xác thực IPN

Để tự động kiểm thử với đơn demo, xem API mô phỏng thanh toán. API đó ghi nhận đơn rồi để worker gửi IPN; khác request Postman giả lập callback trực tiếp tới hệ thống của bạn.

IPN sử dụng m2signature, với chuỗi khác request create:

accessKey={accessKey}&amount={amount}&extraData={extraData}&message={message}&orderId={orderId}&orderInfo={orderInfo}&orderType={orderType}&partnerCode={partnerCode}&payType={payType}&requestId={requestId}&responseTime={responseTime}&resultCode={resultCode}&transId={transId}

Dùng accessKey cấu hình ở backend và giá trị nhận trong IPN cho các trường còn lại. Không thay orderInfo bằng giá trị từ request create: V2 có thể trả nội dung thanh toán do Pay2S sinh. Giữ đúng chuỗi extraData nhận được; trường rỗng vẫn phải có &extraData= trong chuỗi ký.

So sánh HMAC bằng hash_equals() (PHP) hoặc timingSafeEqual() (Node.js). Kiểm tra chữ ký nhận là 64 ký tự hex trước khi chuyển thành buffer. SDK có sẵn hàm xác thực IPN.

Chữ ký hợp lệ chưa đủ để giao hàng: đối chiếu partnerCode, orderId, số tiền với đơn đã lưu, kiểm tra resultCode và bảo đảm mỗi thanh toán chỉ xử lý một lần. Không xác nhận thanh toán chỉ dựa vào URL chuyển hướng của trình duyệt.

Kiểm tra thư viện HMAC

Bộ dữ liệu công khai dưới đây chỉ kiểm tra thuật toán, không phải request Pay2S hợp lệ:

secretKey: key
rawSignature: The quick brown fox jumps over the lazy dog
expectedSignature: f7bc83f430538424b13298e6aa6fb143ef4d59a14946175997479dbc2d1a3cd8
createHmac('sha256', 'key')
  .update('The quick brown fox jumps over the lazy dog', 'utf8')
  .digest('hex');
hash_hmac('sha256', 'The quick brown fox jumps over the lazy dog', 'key');

Chẩn đoán lỗi chữ ký

Hiện tượng Kiểm tra
V1 cũ ngừng khớp Không tự thêm extraData / signatureVersion vào raw V1 cũ
V2 báo sai chữ ký Payload có signatureVersion: "2"; ký cùng extraData đã gửi
V2 có extraData: "" V2 yêu cầu metadata Base64 không rỗng
Sai khi thêm ngân hàng Raw create dùng bankAccounts=Array, không JSON.stringify mảng
Cùng dữ liệu nhưng chữ ký khác Kiểm tra thứ tự trường, UTF-8, URL, xuống dòng, khoảng trắng, định dạng số
Ký đúng nhưng không xác thực được Bộ accessKey/secretKey có thuộc tài khoản hoặc cửa hàng tương ứng partnerCode không
IPN luôn không khớp Dùng m2signature và chuỗi IPN; không thay bằng chuỗi create
Hủy đơn không khớp Ký mới với requestType=cancel

Debug bằng cách so sánh chuỗi trước HMAC ở môi trường kiểm thử. Không đưa secretKey, thông tin khách hàng hay request thật lên log công khai hoặc công cụ tạo HMAC bên ngoài.

Tìm trong 57 trang tài liệu · Esc để đóng