Ở bài trước Odoo gọi ra một dịch vụ. Nhưng nhiều sự kiện đến từ phía kia: cổng thanh toán báo "đã thu tiền", đơn vị giao hàng báo "đã giao xong". Có hai cách để Odoo biết. Cách vụng: polling — Odoo cứ 30 giây lại hỏi "xong chưa?", tốn tài nguyên và luôn trễ. Cách gọn: webhook — bên kia chủ động POST vào một URL của Odoo ngay khi sự kiện xảy ra. Bài này dựng một webhook thật và giải quyết câu hỏi khó nhất của nó: làm sao biết cú POST này đúng là từ đối tác, không phải kẻ giả mạo?

Webhook là một controller, nhưng khác trang web thường

Webhook chỉ là một @http.route nhận POST. Nhưng nó khác controller web bình thường ở ba chỗ quan trọng — và bỏ sót là hỏng:

Ảnh chụp mã Python nền tối controller webhook trong quan_ca_phe. Import hashlib hmac json logging, from odoo import http, from odoo http import request. WEBHOOK_SECRET là bytes chú thích thực tế nên lưu ở ir config_parameter. Class WebhookThe kế thừa http Controller. Decorator http route đường dẫn webhook the type http auth public methods POST csrf False save_session False. Method nhan_su_kien đọc thân thô bytes bằng request httprequest get_data, lấy header X-Signature, tính sig_dung bằng hmac new WEBHOOK_SECRET raw sha256 hexdigest. Nếu not hmac compare_digest sig_gui sig_dung thì trả make_json_response error chu ky sai status 403. Chữ ký hợp lệ mới json loads raw, search thẻ theo name bằng sudo, cộng điểm the diem, trả make_json_response status ok diem_moi

Hình 1: Controller webhook. Ba điểm khác biệt sống còn: (1) csrf=False — hệ thống bên ngoài không có token CSRF của bạn nên bật kiểm CSRF là chặn luôn webhook thật; (2) auth='public' — không có phiên đăng nhập nào cả; (3) vì hai điều trên, phải tự xác thực bằng chữ ký, chứ không dựa vào session.

Trái tim: xác thực bằng HMAC-SHA256

csrf=False + auth='public' nghe như mở toang cửa. Không phải — cửa được khóa bằng chữ ký. Cả hai phía cùng biết một secret chung. Bên gửi tính HMAC-SHA256(secret, thân_request) và đính vào header X-Signature. Phía Odoo tính lại chữ ký đó từ chính thân request nhận được rồi so. Khớp thì đúng là đối tác (chỉ ai có secret mới tạo được chữ ký hợp lệ); lệch thì từ chối.

Hai chi tiết dễ sai:

  • Ký trên thân THÔ (bytes), không phải bản đã parse. Phải request.httprequest.get_data() lấy đúng chuỗi byte gốc; nếu parse JSON rồi serialize lại, một dấu cách khác là chữ ký lệch.
  • So bằng hmac.compare_digest, không phải ==. So chuỗi thường dừng ngay ký tự đầu khác nhau, để lộ thời gian → tấn công đo thời gian (timing attack) đoán dần chữ ký. compare_digest (và odoo.tools.consteq) so trong thời gian hằng định.

Chạy thật: đúng chữ ký thì cộng điểm, sai thì 403

Mình cắm controller này vào module quan_ca_phe thật (thêm controllers/, khởi động lại Odoo), rồi curl POST hai lần:

Ảnh chụp terminal nền tối curl POST tới webhook the chạy thật. Đầu tiên tính chữ ký HMAC-SHA256 của thân request bằng openssl dgst sha256 hmac secret, ra chuỗi hex 0e6ba564. Phần 1 POST với chữ ký đúng trả về json status ok the VIP-0004 diem_cu 0 diem_moi 25 và HTTP 200 chú thích thẻ được cộng 25 điểm thật. Phần 2 POST với chữ ký sai deadbeef000wrong trả về json error chu ky sai và HTTP 403 chú thích bị từ chối không đụng tới dữ liệu

Hình 2: Thật. Tính chữ ký bằng openssl, rồi POST. Chữ ký đúng → HTTP 200, {"diem_cu":0,"diem_moi":25} — thẻ VIP-0004 được cộng 25 điểm thật. Chữ ký sai → HTTP 403, {"error":"chu ky sai"} — bị chặn ngay, không hề đụng tới dữ liệu. (Sau khi chụp, mình đã hoàn nguyên: điểm về 0, gỡ controller.)

Trả 200 nhanh, việc nặng đẩy ra sau

Một quy tắc vận hành: webhook phải trả lời nhanh. Bên gửi thường có timeout ngắn (vài giây) và sẽ thử gửi lại nếu không nhận được 200 kịp. Nếu bạn xử lý nặng ngay trong webhook (gọi API khác, sinh PDF, gửi email), request kéo dài, bên kia tưởng thất bại rồi gửi lại → xử lý trùng.

Cách đúng: webhook chỉ ghi nhận sự kiện (tạo một bản ghi "chờ xử lý") rồi trả 200 ngay; một cron (ir.cron) quét và xử lý phần nặng sau đó. Nhờ đó cũng dễ làm idempotent — lưu id sự kiện để lần gửi lại thứ hai không cộng điểm hai lần.

# webhook: chỉ ghi nhận rồi trả 200 ngay
su_kien = request.env['quan.webhook.log'].sudo().create({
    'event_id': data['id'],       # id duy nhất từ bên gửi
    'payload': raw,
    'trang_thai': 'cho_xu_ly',
})
return request.make_json_response({'status': 'received'})
# cron xử lý sau, và kiểm event_id để không làm trùng

Polling hay webhook?

Polling (Odoo hỏi) Webhook (bên kia gõ cửa)
Ai khởi xướng Odoo, theo lịch Hệ thống khác, khi có sự kiện
Độ trễ Bằng chu kỳ hỏi (30s, 5 phút…) Gần như tức thì
Tài nguyên Tốn: hỏi cả khi không có gì Tiết kiệm: chỉ chạy khi có việc
Xác thực Odoo mang credential đi Phải tự ký/kiểm chữ ký

Webhook gần như luôn tốt hơn khi bên kia hỗ trợ nó. Chỉ quay lại polling khi đối tác không gửi webhook.

Ba ý mang về

  1. Webhook là một controller nhận POST với csrf=False và auth='public' — vì hệ thống bên ngoài không có token CSRF lẫn phiên đăng nhập của bạn.
  2. Bù lại, phải tự xác thực bằng chữ ký HMAC-SHA256: ký trên thân thô (bytes), so bằng hmac.compare_digest (chống timing attack). Không có phiên → chữ ký là bằng chứng danh tính duy nhất.
  3. Trả 200 thật nhanh, đẩy việc nặng sang cron/hàng đợi, và làm idempotent theo id sự kiện để lần gửi lại không xử lý trùng.

Dựng được tích hợp cả hai chiều rồi, phần còn lại của sê-ri chuyển sang một mảng không kém quan trọng: giữ cho code không hỏng khi sửa. Phần sau bắt đầu với TransactionCase — lớp nền để viết kiểm thử tự động trong Odoo, mỗi test chạy trong một giao dịch riêng rồi tự cuộn ngược.