Chữ ký biểu mẫu HTML
Người mua có thể sửa bất kỳ trường nào của biểu mẫu ngay trong trình duyệt — ví dụ, đặt số tiền thấp hơn. Vì vậy các trường hóa đơn đi trong container đã ký: dịch vụ tính lại chữ ký và, nếu không khớp, không tạo hóa đơn.
Máy chủ của cửa hàng tính chữ ký. Chỉ cặp data và signature hoàn chỉnh mới đi đến trình duyệt.
Ký cái gì
data = base64url(json)
signature = HMAC-SHA256(data, formSecret)
HMAC tính trên chuỗi data, không phải trên các byte JSON. Bên nhận xác minh chữ ký với đúng chuỗi được gửi, rồi mới giải mã nó.
Từ đó suy ra không có chuẩn hóa: thứ tự khóa, thụt lề JSON và kiểu thoát Unicode không ảnh hưởng đến chữ ký. Bất kỳ bộ mã hóa JSON chuẩn nào cũng dùng được.
Bộ tham chiếu
Đưa các giá trị này vào mã của bạn và so sánh kết quả.
Khóa bí mật ví dụ:
dvc3khto5WpjEPojZfYJsbJzFEPPVnRa
Số tiền và thời hạn thanh toán dưới dạng số; chuỗi cũng được chấp nhận:
{"projectId":"a1b2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d","amountFiat":10.5,"currencyFiat":"USD","timeToPay":1,"description":"Order #7, delivery","serviceData":"order-7"}
Chuỗi data là base64url của các byte đó. Ở đây phần đệm đã được lược bỏ; bên nhận cũng chấp nhận có phần đệm:
eyJwcm9qZWN0SWQiOiJhMWIyYzNkNC01ZTZmLTRhN2ItOGM5ZC0wZTFmMmEzYjRjNWQiLCJhbW91bnRGaWF0IjoxMC41LCJjdXJyZW5jeUZpYXQiOiJVU0QiLCJ0aW1lVG9QYXkiOjEsImRlc2NyaXB0aW9uIjoiT3JkZXIgIzcsIGRlbGl2ZXJ5Iiwic2VydmljZURhdGEiOiJvcmRlci03In0
Chữ ký của chuỗi này với khóa bí mật ở trên:
09628020c47400c9231420450af642adfdb3c246f33e3e12cb8e28f96c2a785e
Với các giá trị này, những ví dụ bên dưới in ra đúng data và chữ ký này. Cùng JSON đó nhưng thêm khóa "output":"errors" ở cuối sẽ cho chuỗi data khác và chữ ký khác.
Nếu data không khớp trong khi JSON trông giống nhau, nguyên nhân là bộ mã hóa JSON: thứ tự khóa khác hoặc khoảng trắng sau dấu phân tách. Bên nhận vẫn chấp nhận container như vậy, nhưng nó sẽ không trùng khớp từng ký tự với tham chiếu.
Ví dụ
- PHP
- TypeScript
- JavaScript
- Python
<?php
function signedContainer(array $fields, string $formSecret): array {
$data = rtrim(strtr(base64_encode(json_encode($fields)), '+/', '-_'), '=');
return [
'data' => $data,
'signature' => hash_hmac('sha256', $data, $formSecret),
];
}
// Test values from the reference set. Before going live,
// replace the secret with the form secret from your project settings
$container = signedContainer([
'projectId' => 'a1b2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d',
'amountFiat' => 10.5,
'currencyFiat' => 'USD',
'timeToPay' => 1,
'description' => 'Order #7, delivery',
'serviceData' => 'order-7',
], 'dvc3khto5WpjEPojZfYJsbJzFEPPVnRa');
echo $container['data'], "\n";
echo $container['signature'], "\n";
import { createHmac } from 'node:crypto';
// Values are JSON strings or numbers, optional keys may be omitted
export interface FormFields {
projectId: string;
amountFiat: string | number;
currencyFiat: string;
timeToPay: string | number;
description?: string;
serviceData?: string;
output?: string;
}
export interface FormContainer {
data: string;
signature: string;
}
export const signedContainer = (fields: FormFields, formSecret: string): FormContainer => {
const data = Buffer.from(JSON.stringify(fields), 'utf8').toString('base64url');
return {
data,
signature: createHmac('sha256', formSecret).update(data, 'utf8').digest('hex'),
};
};
// Test values from the reference set. Before going live,
// replace the secret with the form secret from your project settings
const container = signedContainer({
projectId: 'a1b2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d',
amountFiat: 10.5,
currencyFiat: 'USD',
timeToPay: 1,
description: 'Order #7, delivery',
serviceData: 'order-7',
}, 'dvc3khto5WpjEPojZfYJsbJzFEPPVnRa');
console.log(container.data);
console.log(container.signature);
Đây là mã cho máy chủ của cửa hàng, không phải trình duyệt: chữ ký tính ở nơi lưu khóa bí mật.
import { createHmac } from 'node:crypto';
const signedContainer = (fields, formSecret) => {
const data = Buffer.from(JSON.stringify(fields), 'utf8').toString('base64url');
return {
data,
signature: createHmac('sha256', formSecret).update(data, 'utf8').digest('hex'),
};
};
// Test values from the reference set. Before going live,
// replace the secret with the form secret from your project settings
const container = signedContainer({
projectId: 'a1b2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d',
amountFiat: 10.5,
currencyFiat: 'USD',
timeToPay: 1,
description: 'Order #7, delivery',
serviceData: 'order-7',
}, 'dvc3khto5WpjEPojZfYJsbJzFEPPVnRa');
console.log(container.data);
console.log(container.signature);
import base64
import hashlib
import hmac
import json
def signed_container(fields: dict, form_secret: str) -> dict:
# compact separators: the default ones add spaces after ',' and ':'
body = json.dumps(fields, separators=(',', ':'))
data = base64.urlsafe_b64encode(body.encode('utf-8')).decode().rstrip('=')
return {
'data': data,
'signature': hmac.new(
form_secret.encode('utf-8'), data.encode('utf-8'), hashlib.sha256
).hexdigest(),
}
# Test values from the reference set. Before going live,
# replace the secret with the form secret from your project settings
container = signed_container({
'projectId': 'a1b2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d',
'amountFiat': 10.5,
'currencyFiat': 'USD',
'timeToPay': 1,
'description': 'Order #7, delivery',
'serviceData': 'order-7',
}, 'dvc3khto5WpjEPojZfYJsbJzFEPPVnRa')
print(container['data'])
print(container['signature'])
Đối số separators cần cho việc trùng khớp từng ký tự với tham chiếu. Bên nhận cũng chấp nhận cài đặt JSON mặc định.
Các giá trị ví dụ là ASCII, nhưng mã chạy với mọi Unicode mà không cần sửa. PHP và Python mặc định thoát ký tự ngoài ASCII thành \uXXXX, JavaScript gửi UTF-8 thô — bên nhận chấp nhận cả hai.
Hai lỗi thường gặp
Ký JSON thay vì container. HMAC tính trên chuỗi data, tức là trên base64url. Lỗi này biểu hiện như “mọi thứ đều khớp, nhưng chữ ký thì không”.
Dựng lại container sau khi ký. Chỉ cần một byte của data thay đổi, chữ ký mất hiệu lực. Hãy dựng cặp này ở một chỗ duy nhất trong mã của bạn.
Nếu chữ ký không khớp
Đi dọc chuỗi JSON → data → chữ ký; mắt xích đầu tiên lệch khỏi tham chiếu chính là nguyên nhân. Kiểm tra data trước: nó không phụ thuộc vào khóa bí mật.