Trả lời ngắn cho những câu hỏi về session chúng tôi gặp nhiều nhất trong thực tế: hết hạn, buộc đăng xuất, lưu trữ, chạy nhiều node, và vì sao script tích hợp mất session sau một đêm.

Phần 1 và phần 2 của loạt bài này đã nói về cách session Odoo hoạt động bên trong và những module cộng đồng mở rộng chúng. Bài khép lại loạt bài đi theo hướng ngược lại: những câu hỏi thực tế chúng tôi nhận được, kèm câu trả lời ngắn và đường dẫn về phần giải thích chi tiết. Mọi câu trả lời áp dụng cho Odoo 19.0 trừ khi có ghi rõ phiên bản; phần lớn vẫn đúng từ 16.0 trở đi.


Thời hạn và hết hạn

Một session Odoo tồn tại bao lâu?

Vĩnh viễn, miễn là người dùng còn hoạt động. Odoo chỉ cho session hết hạn khi không hoạt động: mỗi request lại đẩy mốc hết hạn về sau, nên một session dùng hằng ngày sẽ không bao giờ chết. Ngưỡng không hoạt động mặc định là bảy ngày, tính từ Odoo 16.0. Trước 16.0 các con số không khớp nhau: cookie có hiệu lực 90 ngày nhưng máy chủ lại xóa file session không hoạt động sau 7 ngày, nên trên thực tế giới hạn vẫn là 7 ngày. Odoo 16.0 đưa cả hai về cùng mốc 7 ngày [1].

Không có giới hạn thời gian tuyệt đối — không có thiết lập gốc nào nói “bắt đăng nhập lại sau mỗi N ngày bất kể hoạt động”. Nếu chính sách tuân thủ đòi hỏi điều đó, phải viết code tùy chỉnh (nói ở phần dưới).

Đổi ngưỡng không hoạt động của session bằng cách nào?

Đặt tham số hệ thống sessions.max_inactivity_seconds (Settings → Technical → System Parameters). Tham số này chi phối cả Max-Age của cookie lẫn ngưỡng dọn dẹp phía máy chủ [2]. Hai điểm cần lưu ý:

  • File hết hạn chỉ bị xóa khi cron “Base: Auto-vacuum internal data” chạy hằng ngày. Nếu đặt giá trị ngắn, hãy tăng tần suất cron này cho tương xứng.
  • Odoo không kiểm tra tuổi session ở thời điểm request. Trình duyệt sẽ bỏ cookie khi nó hết hạn, nhưng một script giữ được cookie vẫn dùng được session cho tới khi cron xóa file.

Có giới hạn được thời gian sống tối đa của session bất kể hoạt động không?

Không có sẵn. Cách duy nhất để áp một giới hạn cứng — “bắt đăng nhập lại sau mỗi 24 giờ bất kể thế nào” — là một module tùy chỉnh nhỏ: đóng dấu thời điểm đăng nhập thật vào session lúc xác thực, rồi kiểm tra ở mỗi request và buộc đăng xuất khi vượt ngưỡng.

Đặt timeout ngắn rồi mà người dùng vẫn đăng nhập được. Vì sao?

Nhiều khả năng cron auto-vacuum chưa chạy (xem ở trên), hoặc người dùng vẫn đang hoạt động: timeout đếm thời gian không hoạt động, không phải tổng thời gian sống của session. Nếu cần hành vi “đăng xuất sau N phút không dùng” có hiệu lực ngay ở request kế tiếp, hãy dùng module OCA auth_session_timeout — module này kiểm tra thời gian không hoạt động ở từng request thay vì chờ cron (xem phần 2).

Vì sao session ID của tôi đổi giữa ngày?

Đó là soft rotation: cứ sau ba giờ hoạt động, Odoo sinh lại nửa sau của session ID và giữ nguyên 42 byte đầu [3]. Đây là một bước làm mới về bảo mật, người dùng không thấy gì, và token CSRF vẫn sống sót chính nhờ phần đầu được giữ nguyên.

Session POS có hết hạn theo các thiết lập này không?

Không — hoàn toàn là hai khái niệm khác nhau. Session POS là một đối tượng nghiệp vụ (pos.session) theo dõi việc mở và đóng một quầy thu ngân; nó không liên quan gì tới session HTTP nói trong loạt bài này.


Đăng xuất và vô hiệu hóa session

Buộc một người dùng đăng xuất bằng cách nào?

Đổi mật khẩu hoặc vô hiệu hóa tài khoản của họ. Mỗi session lưu một session_token, là giá trị HMAC tính trên login, mật khẩu đã băm và cờ active của người dùng; ở mỗi request Odoo tính lại token đó từ dữ liệu sống trong database, nên bất kỳ thay đổi nào ở các đầu vào này đều vô hiệu hóa ngay toàn bộ session của người dùng [4]. Không cần xóa cache, không cần khởi động lại máy chủ.

Nếu cần buộc đăng xuất từ xa qua một API endpoint mà không đụng tới mật khẩu, hãy theo dõi module auth_session_logout_api Trobz đóng góp cho OCA/server-auth (đang chờ merge, xem phần 2).

Buộc tất cả người dùng đăng xuất cùng lúc bằng cách nào?

Đổi tham số hệ thống database.secret. Đây là khóa gốc của mọi session token, nên đổi nó sẽ vô hiệu hóa tức thì toàn bộ session của mọi người dùng trong database [4]. Đó là cần gạt khẩn cấp khi nghi ngờ hệ thống bị xâm nhập; hãy chuẩn bị tinh thần mọi người dùng đều bị đưa về màn hình đăng nhập.

Khởi động lại Odoo có làm người dùng bị đăng xuất không?

Không. Session nằm trên đĩa (hoặc trong PostgreSQL/Redis với các module ở phần 2), không nằm trong bộ nhớ của tiến trình. Khởi động lại, nâng cấp hay tái tạo worker đều không đụng tới chúng.

Có xem được người dùng đang đăng nhập từ thiết bị hay IP nào không?

Một phần. Odoo ghi nhật ký thiết bị theo từng session trong res.device.log: nền tảng, trình duyệt, IP, mốc thời gian hoạt động đầu và cuối, đúng như tính năng “Connected devices” đã nhắc ở phần 1 [7]. Dữ liệu được ghi tự động ở các request đã xác thực và truy vấn được như mọi model khác, nên khá hữu ích khi rà soát bảo mật. Thứ nó không cho sẵn là nút “đăng xuất thiết bị này” để người dùng tự bấm — cờ revoked trên mỗi bản ghi chỉ là sổ sách framework tự cập nhật khi một session biến mất, không phải cần gạt để chấm dứt session. Muốn thật sự đẩy một session cụ thể ra ngoài, hãy quay lại cách buộc đăng xuất ở trên (đổi mật khẩu, hoặc auth_session_logout_api đang chờ merge nếu cần đăng xuất chính xác mà không đổi mật khẩu).


Lưu trữ và vận hành

File session nằm ở đâu và có xóa được không?

{data-dir}/sessions/, nằm rải trong tối đa 4096 thư mục con hai ký tự, mỗi session một file JSON [5]. Xóa một file là an toàn và chỉ đơn giản là đăng xuất session đó — xóa sạch tất cả là phiên bản thô bạo của việc đổi database.secret. Từ 16.0 trở đi bạn không cần dọn tay: cron auto-vacuum thu dọn file hết hạn mỗi ngày.

Thư mục sessions cứ phình to. Có bình thường không?

Phình một mức nào đó là bình thường: mỗi khách truy cập, mỗi health check, mỗi API client khi trở thành “dirty” đều sinh một file session. File cũ hơn ngưỡng không hoạt động lẽ ra phải được cron auto-vacuum dọn hằng ngày. Nếu không, hãy kiểm tra cron có chạy không, và kiểm tra biến môi trường ODOO_SKIP_GC_SESSIONS — khi biến này được đặt, Odoo bỏ qua hoàn toàn phần GC session có sẵn và mặc định là sẽ có thứ khác lo dọn [6].

Từ lúc thêm máy chủ thứ hai, người dùng bị đăng xuất ngẫu nhiên. Chuyện gì đã xảy ra?

Session store trên filesystem là cục bộ theo từng máy: session tạo trên node A không tồn tại trên node B, nên request rơi vào nhầm node sẽ bị đẩy về trang đăng nhập. Cần một store dùng chung — session_db (PostgreSQL) hoặc session_redis, cả hai đều được nói tới ở phần 2. Đây là vấn đề session chúng tôi gặp nhiều nhất ở các hệ thống chạy nhiều node.

Sticky session có thay được session_db hay session_redis không?

Sticky session (định tuyến một client luôn về cùng một node bằng ip_hash hoặc cookie của reverse proxy) là cách đi vòng quanh chuyện session store cục bộ theo máy mà không cần dựng store dùng chung. Cách này tránh được việc thêm hạ tầng mới, nhưng đánh đổi đúng lý do bạn thêm node thứ hai ngay từ đầu: một node hỏng, hay một lần deploy tái tạo worker, sẽ làm mất toàn bộ session bị ghim vào node đó, và tải cũng thôi được chia đều khi một số client dính chặt vào một node đang bận. session_db hoặc session_redis loại bỏ hẳn yêu cầu ghim node, đổi lại là thêm một thành phần phụ thuộc. Với quy mô lớn hơn một cụm nhỏ, nên chọn store dùng chung.

Có đọc được file session để debug không?

Có: đó là JSON thuần. Trong file có uid, db, login, context của người dùng, session_token, các cờ debug và dữ liệu vết thiết bị. Phần 1 mô tả đầy đủ từng trường. Đừng sửa file bằng tay — session_token ràng buộc nội dung file với trạng thái database, và chỉ cần lệch là session mất hiệu lực.


Bảo mật

Một cookie session_id bị đánh cắp đúng là cho phép truy cập trong thời gian session còn hiệu lực — chính vì vậy cookie này là HttpOnly (JavaScript không đọc được) và session được hard-rotate ở cả lúc đăng nhập lẫn đăng xuất. Cần gạt để xử lý là session_token: đổi mật khẩu của nạn nhân sẽ cắt đứt kẻ tấn công ngay lập tức, vì token tính lại không còn khớp nữa [4].

Session có dùng chung giữa các database không?

Không. Mỗi session gắn với đúng một database qua trường db, và session_token được ký bằng chính database.secret của database đó. Trên một máy chủ nhiều database, đăng nhập vào database thứ hai sẽ thay thế session đang có.

Có nhúng được một trang Odoo đã đăng nhập vào iframe trên site khác không?

Với backend thì không: Odoo gửi X-Frame-Options: SAMEORIGIN trên các trang backend một cách có chủ đích, nên trình duyệt từ chối render chúng trong frame khác site. Với những trang vốn được thiết kế để nhúng (trang portal hoặc website), bạn vẫn vướng chính cái cookie: session_id được đặt mà không khai báo SameSite [8], nên trình duyệt mặc định coi là Lax — và cookie Lax không được gửi kèm trong request khác site do iframe của site khác khởi tạo. Muốn cookie session chạy được khác site thì phải cố ý đặt SameSite=None; Secure cho nó, điều Odoo không làm với cookie xác thực, vì như vậy sẽ làm yếu khả năng chống CSRF cho toàn bộ session. Nếu thật sự cần nhúng khác site, hãy tính hướng thu hẹp trang được phơi ra thành thứ không phụ thuộc session_id (token dùng một lần có chữ ký, token public/portal) thay vì tìm cách nới lỏng cookie.


Tích hợp và tự động hóa

Script tích hợp của tôi xác thực một lần, vài giờ sau báo “session expired”. Vì sao?

Session của script chịu đúng quy tắc không hoạt động như của trình duyệt: nếu script không đụng tới session lâu hơn sessions.max_inactivity_seconds — nhiều khả năng vì nó giữ cookie nhưng không gửi thêm request đã xác thực nào trong một quãng xử lý dài, hoặc vì cron auto-vacuum đã dọn mất file — thì lần gọi tiếp theo sẽ lỗi. Đây không phải đặc thù của JSON-RPC hay XML-RPC; vẫn là cơ chế hết hạn phía máy chủ nói ở trên, chỉ là do một client không thử lại nên gặp phải.

Cách xử lý cũng giống như với mọi token ngắn hạn: đừng cho rằng một lần gọi /web/session/authenticate là dùng được mãi. Với các tác vụ chạy dài, hoặc xác thực lại khi nhận phản hồi session expired, hoặc kiểm tra session còn hiệu lực trước mỗi lô (ví dụ bằng một lời gọi đã xác thực nhẹ nhàng) rồi chủ động xác thực lại nếu hỏng, thay vì phát hiện ra giữa chừng.


Những điểm chính

Session Odoo không hết hạn theo đồng hồ — chúng hết hạn khi không hoạt động, nghĩa là một session đang được dùng có thể sống vô thời hạn, kể cả session của script tích hợp, vốn cần xác thực lại trong các tác vụ dài. Ngưỡng không hoạt động nằm ở một tham số hệ thống (sessions.max_inactivity_seconds), và nhớ rằng việc dọn dẹp do một cron chạy hằng ngày thực thi chứ không phải ở thời điểm request. Muốn có giới hạn thời gian sống tuyệt đối, độc lập với hoạt động, phải viết code tùy chỉnh. Buộc đăng xuất là đổi mật khẩu (một người dùng) hoặc đổi database.secret (tất cả); res.device.log cho thấy được các session đang hoạt động nhưng không cho cách tắt một session chỉ bằng một cú nhấp. Khởi động lại không bao giờ làm người dùng đăng xuất; thêm node thứ hai mà không có session store dùng chung thì luôn luôn có, và sticky session chỉ che đi vấn đề đó chứ không giải quyết trọn vẹn.

Quay lại loạt bài: Phần 1 đi sâu vào cơ chế bên trong của session; phần 2 nói về session_db, session_redis và các module quản lý vòng đời.


Nguồn

[1] odoo/http.py:307-308 (Odoo 19.0, SESSION_LIFETIME, 7 ngày) và commit cea9150fb7 của odoo/odoo (“make session lifetime consistent and configurable”, 16.0)

[2] odoo/http.py:452-461: get_session_max_inactivity()

[3] odoo/http.py:1008-1040: rotate() (soft/hard rotation)

[4] odoo/addons/base/models/res_users.py:851-884: _compute_session_token()

[5] odoo/http.py:968-1006: FilesystemSessionStore

[6] odoo/addons/base/models/ir_http.py:406-410: cron autovacuum _gc_sessions(), ODOO_SKIP_GC_SESSIONS

[7] odoo/addons/base/models/res_device.py:17-38, 84-165: các trường của res.device.log, update_trace(), sổ sách thu hồi

[8] odoo/http.py:1757-1763: Response.set_cookie() — không truyền SameSite tường minh cho session_id, nên rơi về mặc định của trình duyệt (Lax)