框架深潜

Odoo 多数据库为什么经常不是‘选中一个库就完了’:session、dbfilter、X-Odoo-Database 与 monodb 路由边界讲透

很多人把 Odoo 的多数据库路由理解成一个登录前的数据库选择器,但 http.py 源码说明真实决策顺序要复杂得多:session 里已有的数据库能不能继续被信任、Host 是否匹配 dbfilter、X-Odoo-Database 何时能生效、只有一个库时为什么会自动 monodb,以及旧 session 为什么会被直接踢出。

框架
进阶 开发者 2 分钟阅读
0 评论 0 点赞 0 收藏 5 阅读

先说结论

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) 非常关键。

它会:

  1. 读取 config['dbfilter']
  2. 从当前 HTTP_HOST 提取 host
  3. 去掉端口
  4. 去掉前缀 www.
  5. 再取第一个子域片段作为 %d
  6. %h%d 替换进正则
  7. 只返回匹配的数据库列表

这说明 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 下可见库数量

dbfilterdb_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 不生效”,建议按这个顺序查:

  1. 先看当前 Host 经过 dbfilter 后,到底暴露了哪些数据库
  2. 再看 session 里原有 session.db 是否还被当前 Host 接受
  3. 如果用了 X-Odoo-Database,确认它是否也通过 dbfilter,以及是否被无状态处理
  4. 检查当前 Host 视角下是不是其实进入了 monodb 自动选库
  5. 最后再看反向代理、域名切换或测试环境克隆是否改变了请求看到的 Host。

一句话记忆

Odoo 多数据库路由不是“你想去哪库就去哪库”,而是“在当前 Host、当前会话与当前暴露策略下,哪个数据库还值得被继续信任”。

DISCUSSION

评论区

想参与讨论?先 登录 再发表评论。
还没有评论,你可以成为第一个留言的人。