Ở phần trước ta dựng controller và lướt qua auth. Nhưng auth đáng một bài riêng, vì nó là công tắc bảo mật quan trọng nhất của một route: chọn sai là mở toang một endpoint đáng lẽ phải đăng nhập, hoặc khoá chặt một trang đáng lẽ ai cũng xem được. Để thấy rõ khác biệt, tôi dựng ba route có thân hàm y hệt nhau — cùng trả về "ai đang chạy" — chỉ khác mỗi auth, rồi curl cả ba khi chưa và đã đăng nhập. Kết quả nói lên tất cả.

Ba route, một thân hàm

class AuthDemoController(http.Controller):

    def _ai_dang_chay(self):
        u = request.env.user
        return {'uid': request.env.uid,
                'ten_user': u.name if request.env.uid else None,
                'login': u.login if request.env.uid else None}

    @http.route('/qcp_auth/user',   type='http', auth='user',   csrf=False)
    def route_user(self, **kw):   return self._tra_json(self._ai_dang_chay())

    @http.route('/qcp_auth/public', type='http', auth='public', csrf=False)
    def route_public(self, **kw): return self._tra_json(self._ai_dang_chay())

    @http.route('/qcp_auth/none',   type='http', auth='none',   csrf=False)
    def route_none(self, **kw):   return self._tra_json(self._ai_dang_chay())

Ảnh chụp mã Python nền tối class AuthDemoController kế thừa http Controller. Phương thức _ai_dang_chay lấy request env user và trả về dict gồm uid bằng request env uid, ten_user bằng tên user nếu có uid ngược lại None, login tương tự. Ba route cùng thân hàm gọi _ai_dang_chay chỉ khác auth: route qcp_auth user auth user bắt buộc đã đăng nhập user thật không phải public; route qcp_auth public auth public ai cũng vào chưa đăng nhập thì Public user dùng chung; route qcp_auth none auth none luôn chạy không có current user uid None không cần cả db

Hình 1: Ba route giống hệt nhau về logic, chỉ khác auth. Cùng gọi request.env.user — nhưng như ta sẽ thấy, "user" trong mỗi route là một người khác nhau, hoặc không có ai cả.

Kết quả thật: chưa và đã đăng nhập

curl cả ba route ở hai trạng thái, đây là kết quả sống trên hệ thống:

Ảnh chụp terminal nền tối curl thật tới ba route trên Odoo 19 blog19 so hai trạng thái. Khi CHƯA đăng nhập: qcp_auth user trả về mã 303 chuyển hướng tới web login bị chặn; qcp_auth public trả về uid 3 ten_user Public user login public; qcp_auth none trả về uid null ten_user null login null. Khi ĐÃ đăng nhập admin: qcp_auth user trả về uid 2 ten_user Administrator login admin; qcp_auth public cũng trả về uid 2 Administrator login admin; qcp_auth none vẫn trả về uid null dù đã đăng nhập

Hình 2: Cùng ba route, hai trạng thái. auth='user' chưa đăng nhập bị đá về /web/login (303); đăng nhập rồi ra user thật (uid 2, Administrator). auth='public' chưa đăng nhập là Public user (uid 3), đăng nhập rồi thành chính bạn. auth='none' luôn không có user (uid null) — kể cả khi đã đăng nhập.

Ba hành vi rất khác nhau, và mỗi cái có chỗ dùng riêng:

  • auth='user' — endpoint nội bộ, dữ liệu riêng tư, thao tác thay đổi. Bắt buộc đăng nhập; chạy bằng quyền của chính người đó nên Record Rules và Access Rights áp dụng đúng.
  • auth='public' — trang/route công khai mà cũng muốn cá nhân hoá nếu khách có đăng nhập: trang giỏ hàng, trang chi tiết sản phẩm, form liên hệ. Khách vãng lai chạy bằng "Public user" quyền hẹp; khách đã đăng nhập thấy dữ liệu của mình.
  • auth='none' — hạ tầng: health-check, webhook tự xác thực bằng chữ ký riêng, endpoint của chính khung xác thực. Chạy kể cả khi chưa chọn database.

Vì sao "user" chặn cả Public user

Điểm dễ hiểu nhầm: auth='user' không chỉ đòi "có một user nào đó" — nó đòi một user thật, loại trừ Public user. Nhìn vào lõi ir_http.py của Odoo 19 là rõ:

Ảnh chụp mã Python nền tối file base models ir_http.py Odoo 19 cơ chế thật sau mỗi auth. Hàm _auth_method_user kiểm nếu request env uid nằm trong danh sách None cộng với _get_public_users thì raise SessionExpiredException, chú thích uid None hoặc là public thì chặn phải là user thật. Hàm _auth_method_public kiểm nếu request env uid là None thì lấy ref base public_user và update_env sang user đó, chú thích chưa đăng nhập thì gán Public user dùng chung. Hàm _auth_method_none gán request env bằng Environment mới với uid None, chú thích không current user chạy cả khi không có db

Hình 3: Cơ chế thật. _auth_method_user ném SessionExpiredException nếu uid là None hoặc nằm trong danh sách public users — nên một phiên "public" vẫn bị chặn. _auth_method_public gán base.public_user khi chưa đăng nhập. _auth_method_none dựng lại environment với uid=None, không có ai.

Đó là lý do auth='none' trả uid: null cả khi bạn đã đăng nhập (Hình 2): nó chủ động vứt bỏ user context. Route none không có khái niệm "người dùng hiện tại"; nếu code trong đó cần đọc dữ liệu, bạn phải tự tạo environment với một user cụ thể qua request.env(user=...) hoặc sudo().

Bearer: mức mới của Odoo 19

Odoo 19 thêm auth='bearer' cho API máy-gọi-máy: client gửi header Authorization: Bearer <api_token>, route chạy với quyền của người sở hữu token. Nếu thiếu header, nó lùi về như auth='user' (đòi phiên đăng nhập). Đây là cách sạch sẽ để lộ API cho hệ thống ngoài mà không phải nhét mật khẩu vào mỗi request hay tự chế cơ chế token.

Cạm bẫy hay gặp

  • Đừng để auth='public' cho route ghi dữ liệu nhạy cảm. Public user quyền hẹp nhưng vẫn là một danh tính chạy được; một route công khai vô tình cho ghi là lỗ hổng. Route thay đổi dữ liệu gần như luôn phải auth='user' (hoặc bearer).
  • auth='public' không có nghĩa "bỏ qua quyền". Public user vẫn chịu Access Rights/Record Rules của nó; nếu route cần đọc dữ liệu mà Public user không thấy, phải sudo() có chủ đích — và khi sudo(), bạn tự chịu trách nhiệm lọc dữ liệu trả ra.
  • auth='none' thì request.env.user vô nghĩa. Đừng gọi nó mà không dựng environment; đây là nguồn của lỗi "không có user" khó hiểu.
  • CSRF vẫn phải để ý. auth và csrf là hai trục khác nhau: một route auth='public' nhận POST từ form web vẫn nên bật CSRF; chỉ tắt cho endpoint API/webhook.

Ba ý mang về

  1. auth quyết định ai được gọi và code chạy dưới danh nghĩa ai: user (đăng nhập thật, chặn cả Public user, chạy đúng quyền người đó), public (ai cũng vào — khách là Public user, đã đăng nhập là chính họ), none (luôn chạy, không có current user, kể cả không có db).
  2. auth='none' trả uid=None ngay cả khi đã đăng nhập vì nó dựng lại environment không user — chỉ dùng cho hạ tầng/xác thực, và phải tự tạo env nếu cần đọc dữ liệu.
  3. Chọn theo rủi ro: route ghi/dữ liệu riêng → user (hoặc bearer mới của Odoo 19); trang công khai cá nhân hoá → public kèm sudo() có chủ đích; đừng nhầm auth với CSRF.

Giờ ta biết ai được gọi route, phần sau đi vào thứ hay dựng nhất trên nền đó: Phần sau — controller trả JSON cho JS/API, cách trả dữ liệu gọn gàng cho front-end OWL và cho hệ thống ngoài, và khác biệt giữa trả JSON kiểu http và kiểu jsonrpc.