bash - staycrafting.dev

~$ cat moi-quan-coc-tra-da-gan-nut-donate-bang-sepay-va-webhook.md

Mời quán cốc trà đá: gắn nút donate bằng SePay và webhook

Gắn nút ủng hộ cho blog bằng SePay, và chuyện làm sao biết tin "tiền về" là thật: API Key, OAuth 2.0, HMAC khác nhau thế nào, kể bằng chuyện đời thường.

Cả series Blog trà đá mình ngồi đếm từng nghìn xem blog uống hết bao nhiêu ly trà đá. Hôm nay đảo chiều: anh em mời quán một ly. Quét QR, chuyển khoản, avatar tự hiện dưới bài.

Mình không phải bấm xác nhận ai, chỉ việc ngồi uống.

Bài đi qua cách lắp SePay vào blog, rồi đến phần chính: tin "tiền về" gửi tới blog, làm sao biết là thật?

1. SePay: ông thu ngân không giữ két

Tiền đi thẳng từ ngân hàng người đọc sang ngân hàng của mình. SePay không cầm tiền, chỉ đứng nhìn tài khoản, thấy tiền về thì hô lên cho blog biết.

Mấy món blog dùng tới:

  • Liên kết ngân hàng: SePay đọc biến động số dư, không cần mật khẩu Internet Banking.
  • Tài khoản ảo: một "ngăn kéo" riêng trong tài khoản thật, tiền trà đá không lẫn tiền lương.
  • QR VietQR: ảnh QR có sẵn số tài khoản và nội dung chuyển khoản, app ngân hàng nào cũng quét được.
  • Webhook: có tiền về là SePay gọi sang blog báo tin, blog chưa trả lời "đã nhận" thì gọi lại. Kiên trì như mẹ gọi về ăn cơm.

Giá: gói FREE 50 giao dịch/tháng, vượt mới tính phí. Blog mà được 50 lượt ủng hộ một tháng thì mình đi trà sữa full topping ăn mừng trước.

2. Lắp vào website

Phía SePay: tạo tài khoản gói FREE, liên kết ngân hàng (không đưa mật khẩu, không nhận OTP hộ, SePay chỉ cần nhìn), rồi tạo một tài khoản ảo làm ngăn kéo cho tiền trà đá. Tùy ngân hàng, ngăn kéo là một số tài khoản riêng (VA) hoặc một mã trong nội dung chuyển khoản (TKP).

Cuối cùng là tạo webhook trỏ về blog: chỉ báo tiền vào (tiêu tiền thì không ai vinh danh), khai tiền tố mã SC để SePay nhặt mã ủng hộ ra khỏi nội dung chuyển khoản. Ô quan trọng nhất là xác thực, đọc mục 3.

Phía blog làm ba việc: sinh mã ủng hộ cho mỗi lượt bấm, ghép link QR, nhận tin từ SePay để đánh dấu "đã ủng hộ". SePay báo một tin mấy lần thì blog cũng chỉ ghi sổ một lần.

3. Tin "tiền về" có thật không?

Địa chỉ webhook công khai, ai cũng gửi vào được. Giống bill giả khi bán hàng online: ảnh chuyển khoản thành công, tiền thì không thấy đâu.

Hình dung blog có quầy lễ tân, SePay cử người mang tới tờ thông báo "Vừa nhận 20 nghìn, nội dung SC7K2Q9X". Lễ tân không tận mắt thấy tiền về, ai cũng viết được một tờ y hệt. Dựa vào đâu để tin? SePay có bốn lựa chọn.

Cách 0: None, tờ nào cũng nhận

Như chủ shop tin mọi ảnh bill khách gửi. SePay khuyến nghị không dùng.

Dịch cabin ngược sang kỹ thuật: request không kèm header bảo mật nào. Ai curl được vào URL là giả được giao dịch.

Cách 1: API Key, mật khẩu chung

Người mang tờ thông báo phải đọc đúng mật khẩu thì lễ tân mới nhận. Đơn giản, nhưng ai nghe lỏm được một lần là dùng được mãi, và mật khẩu đúng không có nghĩa là nội dung tờ giấy chưa bị sửa.

Dịch cabin ngược sang kỹ thuật: mỗi request kèm header Authorization: Apikey <API_KEY>, server so với key đã lưu, sai thì trả 401. Key cố định, lộ qua log hay ảnh chụp là phải đổi, và không gắn gì với body.

Cách 2: OAuth 2.0, đổi CCCD lấy thẻ khách

Như lên tòa văn phòng: trình CCCD ở quầy bảo vệ, nhận thẻ khách có hạn, chìa thẻ ở quầy lễ tân. Hết hạn thì đổi thẻ mới.

1. Xin thẻ:    SePay ── client_id + client_secret ──► Token endpoint (blog dựng)
               SePay ◄── access_token (1 giờ) + refresh_token
2. Báo tin:    SePay ── webhook + Bearer <access_token> ──► Blog kiểm token còn hạn
3. Đổi thẻ:    SePay ── refresh_token ──► Token endpoint ──► access_token mới

Lộ token thì kẻ gian chỉ dùng được đến lúc hết hạn. Đổi lại, blog phải tự dựng token endpoint, và token cũng không gắn với nội dung.

Cách 3: HMAC-SHA256, mã kiểm tra tính từ nội dung

SePay và blog cùng biết một công thức bí mật, không bao giờ gửi đi. Với mỗi tờ thông báo, SePay dùng công thức đó tính ra một mã từ nội dung và giờ gửi:

Nhận 20.000đ, nội dung SC7K2Q9X, lúc 09:15     Mã: 4821

Lễ tân tự tính lại, ra 4821 thì nhận. Kẻ gian sửa thành "200.000đ" nhưng không biết công thức, mã lệch, loại. Mang tờ cũ ra nộp lại cũng loại, vì giờ gửi đã quá 5 phút.

SePay khuyên dùng cách này, blog mình cũng chọn nó.

HMAC-SHA256 là gì?

SHA-256 là máy xay sinh tố: bỏ gì vào cũng ra một "ly" 64 ký tự, gọi là mã băm (hash). Cùng nguyên liệu thì cùng ly, đổi một chút là ra ly khác hẳn, nhìn ly không đoán ngược ra nguyên liệu. Nhưng máy này ai cũng có: kẻ gian sửa số tiền rồi tự xay lại là ra mã khớp.

HMAC là bỏ thêm nguyên liệu bí mật, chính là Secret Key. Không có key thì không xay ra được đúng ly đó.

Dịch cabin ngược sang kỹ thuật: "ly sinh tố" là chữ ký. Mỗi request kèm hai header:

  • X-SePay-Timestamp: giờ gửi.
  • X-SePay-Signature: chữ ký dạng sha256=..., xay từ giờ gửi + body + Secret Key.

Blog xay lại từ đúng giờ gửi và body đó, cộng Secret Key của mình. Giống nhau thì nhận.

Hai lưu ý khi tự code:

  • Xay đúng body gốc (trong Rails là request.raw_post). Đọc JSON ra rồi ghép lại, thừa một dấu cách là ra ly khác.
  • So bằng secure_compare, đừng dùng ==. == sai ở ký tự nào là dừng ở đó, kẻ gian đo thời gian phản hồi có thể đoán dần chữ ký (timing attack).

Lộ chữ ký thì sao? Token lộ là kẻ gian gắn được vào bất kỳ nội dung nào trong một giờ. Chữ ký chỉ khớp đúng tờ đó: đổi một chữ là lệch, quá 5 phút là hết hạn, nộp lại y nguyên thì blog chỉ ghi một lần vì mỗi giao dịch SePay có mã riêng. Chỉ lộ Secret Key mới toang. Cất nó trong Secret Manager, nghi lộ thì tạo key mới trên SePay rồi đổi bên blog ngay.

Gói lại một bảng

Mật khẩu chung Thẻ khách có hạn Mã kiểm tra
Tên thật API Key OAuth 2.0 HMAC-SHA256
Cho biết Ai gửi Ai gửi (thẻ có hạn) Ai gửi, nội dung nguyên vẹn, gửi lúc nào
Bị lộ thì Dùng được mãi Dùng được một lúc Mã cũ vô dụng
Phải dựng thêm Không Quầy bảo vệ phát thẻ Không
Header Authorization: Apikey ... Authorization: Bearer ... X-SePay-Signature + X-SePay-Timestamp
Phía server kiểm So chuỗi key Kiểm token còn hạn Tự ký lại body thô, so chữ ký, kiểm giờ

Một chỗ hay nhầm: ba cách trên không phải mã hóa, ai cầm tờ thông báo cũng đọc được. Chúng chỉ cho biết tờ đó có đúng của SePay không, có bị sửa không. Chống đọc trộm trên đường là việc của HTTPS.

Mẹo nhỏ: trang xác thực webhook của SePay có code mẫu PHP, Node.js, Python để thử luôn.

Túm cái váy lại

Làm webhook thì cứ hỏi ba câu: ai gửi, gửi gì, gửi lúc nào. API Key và OAuth chỉ trả lời câu đầu, HMAC trả lời cả ba.

Chúc anh em webhook luôn báo đúng, mã kiểm tra luôn khớp, và trà luôn đủ đá.

À mà, nút ủng hộ ở ngay bên dưới. Không phải mình xin đâu, mình chỉ đang test production thôi.

~$ cd ..