先说结论
Odoo 的多数据库路由,并不是“登录前选一下数据库”这么简单。
从 /home/ubuntu/odoo-temp/odoo/http.py 可以看出,请求落到哪个数据库,至少要同时看:
- 当前 session 里有没有记住旧
session.db - 当前 Host 经过
dbfilter后允不允许这个库 - 请求头里有没有
X-Odoo-Database - 当前 Host 下是否只剩一个库可见,也就是 monodb 场景
所以真正的决策顺序不是“前端选哪个库”,而是:
在当前 Host 与当前请求上下文下,哪个数据库仍然是可被信任、可被暴露、可被延续的那个库。
db_filter() 真正做的是“暴露面裁剪”
http.py 里的 db_filter(dbs, host=None) 非常关键。
它会:
- 读取
config['dbfilter'] - 从当前
HTTP_HOST提取 host - 去掉端口
- 去掉前缀
www. - 再取第一个子域片段作为
%d - 把
%h和%d替换进正则 - 只返回匹配的数据库列表
这说明 dbfilter 不是“登录体验配置”,而是:
- 当前域名下,到底有哪些数据库应该被看见。
为什么这点特别重要
因为多数据库环境里,数据库列表本身就是攻击面的一部分。
如果不做 Host 级收口:
- 一个域名可能暴露出不该看到的其他库;
- 测试库、历史库、临时恢复库可能被前台看见;
- session 或 header 可能把请求带去错误库。
所以 dbfilter 的本质不是便利性,而是 暴露面治理。
session 里的 db 不是永久真理
在请求初始化阶段,Odoo 会先看:
if session.db and db_filter([session.db], host=host):
也就是说,当前 session 里已经记住的数据库,只有在 当前 Host 仍允许它 时,才能继续使用。
这点非常关键。
很多系统一旦登录,就会把“会话绑定数据库”当成永久事实; Odoo 没这么天真。
它的逻辑更像:
- 你之前登录过哪个库我知道;
- 但今天这个 Host 下,这个库还允不允许暴露,我得重新验一遍。
这就是为什么旧 session 会被踢出
如果当前 session.db 不再通过 dbfilter,源码会:
- 打 warning:
dbfilter rejects it; logging session out. session.logout(keep_db=False)- 再把
session.db更新成当前解析结果。
这一步很多人第一次看到会觉得“太狠了”,其实它非常正确。
因为如果 Host 边界已经变了,旧 session 再继续被信任,等于跨域名把旧数据库上下文偷渡过来。
X-Odoo-Database 不是万能后门,而是受约束的无状态选择器
如果没有可继续使用的 session.db,Odoo 才会看:
header_dbname = request.headers.get('X-Odoo-Database')
但即使请求头带了库名,也不是直接采用。
源码还会继续检查:
if db_filter([header_dbname], host=host):
只有这个 header 指定的数据库也通过了当前 Host 的 dbfilter,它才会生效。
这说明什么
说明 X-Odoo-Database 的定位不是“绕过 dbfilter 指哪打哪”,而是:
在当前 Host 允许暴露的范围内,提供一个无状态地显式指定数据库的方法。
这对:
- API 网关
- 反向代理
- 某些多租户中间层
很有用。
但它依然被 Host 边界约束着。
为什么 header 模式下 session.can_save = False
还有一个很容易被忽略的小细节:
当使用 X-Odoo-Database 时,源码会设置:
session.can_save = False # stateless
这非常说明问题。
它表达的是:
- 这次数据库选择是由外部请求头临时声明的;
- 不要顺手把这种临时选择固化进会话。
这是很成熟的边界设计。
否则很容易出现:
- 一次 API 调用借 header 指到了 A 库;
- 结果服务器把 A 库偷偷记进 session;
- 后续浏览器请求又在完全不同上下文下延续了这个库。
官方这里明确选择了:
- header 选库是无状态的,不继承成浏览器式会话事实。
monodb 为什么会自动选库
如果既没有可用的 session.db,也没有 header 指库,Odoo 会走:
all_dbs = db_list(force=True, host=host)- 如果
len(all_dbs) == 1,那就直接选这个库
这就是常说的 monodb。
monodb 的真正含义不是“单库部署”这么简单
更准确地说,它是:
- 在当前 Host 和当前
dbfilter视角下,只剩一个可见库。
所以一个物理实例即使背后有很多数据库,只要当前域名过滤后只暴露一个,用户体验上仍然像单库。
这也解释了为什么有些人会说:
- 同一套 Odoo,有的域名不用选库,有的域名却要选。
原因不一定在数据库数量本身,而在 当前 Host 下可见库数量。
dbfilter 与 db_name 的顺序也很说明问题
在 db_filter() 里,如果配置了 dbfilter,就优先按它筛;
只有没配 dbfilter 时,才会退到 config['db_name']。
而且源码注释还写得很明白:
- 当没有
--db-filter,但传了--database时,Odoo 把它当作暴露数据库集合。
这说明官方更推崇的是:
- 有域名边界时,用
dbfilter; - 没有域名边界时,至少用
db_name收口暴露面。
也就是说,Odoo 对“数据库暴露给谁看”这件事,从设计上就是谨慎的。
多数据库事故,很多其实是“路由语义没想清楚”
实战里经常看到几种典型误解:
误解一:登录成功后 session 里的库就永远可信
不是。Host 一变、dbfilter 一变,旧库就可能被拒绝。
误解二:X-Odoo-Database 可以强行指定任意库
不是。它仍要通过 dbfilter,并且默认按无状态处理。
误解三:只有数据库数量为 1 才叫 monodb
不准确。关键是当前 Host 视角下可见库是否恰好为 1。
误解四:dbfilter 只是为了让登录页更整洁
太低估了。它本质上是在控制数据库暴露面。
实战排查顺序
如果你遇到“为什么突然跳到别的库”“为什么旧会话失效”“为什么某个 header 不生效”,建议按这个顺序查:
- 先看当前 Host 经过
dbfilter后,到底暴露了哪些数据库; - 再看 session 里原有
session.db是否还被当前 Host 接受; - 如果用了
X-Odoo-Database,确认它是否也通过dbfilter,以及是否被无状态处理; - 检查当前 Host 视角下是不是其实进入了 monodb 自动选库;
- 最后再看反向代理、域名切换或测试环境克隆是否改变了请求看到的 Host。
一句话记忆
Odoo 多数据库路由不是“你想去哪库就去哪库”,而是“在当前 Host、当前会话与当前暴露策略下,哪个数据库还值得被继续信任”。
DISCUSSION
评论区