Ở 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:

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:

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ề
- Webhook là một controller nhận POST với
csrf=Falsevà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. - 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. - 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.