先说结论
Odoo 的 TOTP 不是“用户绑定一个二维码,以后多输一位验证码”这么简单。
从 /home/ubuntu/odoo-temp/addons/auth_totp/models/res_users.py、models/auth_totp.py 和 controllers/home.py 可以看出,官方实际实现的是一套完整的二次认证闭环:
- 启用时先验证 secret 和 code 能不能匹配
- 登录时校验验证码是否有效、是否被重放
- 连续失败会触发限流
- 勾选“记住此设备”后会下发受信设备 cookie
- 改密码或手动禁用 TOTP 时,旧设备会被统一撤销
一句话概括:
Odoo 把 TOTP 设计成“持续受约束的认证能力”,而不是一个临时拼上去的 6 位码输入框。
启用 TOTP 时,为什么不是扫完码就算开通
action_totp_enable_wizard() 不会直接把 secret 写进用户表,而是先生成一个新的 secret,交给向导界面展示。
真正决定是否启用的是 _totp_try_setting(secret, code):
- 如果当前用户已经开过 TOTP,拒绝重复开通
- 如果不是本人操作,也拒绝
- secret 会先去掉空白并转成大写
- 只有
TOTP.match(code)成功,才会把totp_secret落库 - 同时把当前计数器写到
totp_last_counter
这个设计很重要,因为它避免了一个常见安全洞:
- 管理员或脚本先把 secret 写进去
- 用户其实还没在手机上成功配置
- 结果账户被置于“看起来启用了 2FA,实际上本人登不进去”的尴尬状态
Odoo 的做法更稳:先证明你手里的验证器真的能产出正确验证码,再把 TOTP 正式打开。
为什么要记录 totp_last_counter
很多人只看“验证码对不对”,但 Odoo 还会检查 match <= totp_last_counter。
这意味着同一个时间片里已经用过的验证码,不能再用第二次。
这不是多余的严苛,而是在防 重放:
- 攻击者截获一次有效验证码
- 如果系统只校验“当前时间片是否正确”,那同一 30 秒窗口里可能还能再进一次
而 Odoo 把上次成功的 counter 存下来后,规则就变成:
- 码要对
- 还必须是“比上次更新的一次更新”
所以它防的不是“输错码”,而是“旧正确码再次被利用”。
为什么连续输错几次就被卡住
_totp_rate_limit() 给 TOTP 相关动作定义了限流:
code_check:1 小时内最多 5 次send_email:1 小时内最多 5 次
限流日志按这些维度记录:
- 用户
- IP
- 动作类型
- 创建时间
这说明 Odoo 的策略不是只看 IP,也不是只看账号,而是把“谁在什么来源做什么动作”当成一条审计记录。
更关键的是,_totp_rate_limit_purge('code_check') 会在成功验证后清掉这一类失败记录。
这背后的产品意图很明显:
- 失败尝试要收紧,防爆破
- 成功认证后要放行,别让合法用户继续被历史失败拖住
这是一种很典型的平台安全平衡。
“记住此设备”为什么不是简单记个浏览器标记
/web/login/totp 这条路由里,如果用户勾选 remember,系统会创建 auth_totp.device 记录,并通过 td_id cookie 把设备密钥写回浏览器。
这里有几个很关键的点:
1. 设备不是布尔值,而是一条密钥记录
auth_totp.device 继承的是 res.users.apikeys 体系。
也就是说,受信设备不是“数据库里写个 trusted=true”,而是复用安全密钥模型来校验:
- 哪个用户
- 什么作用域
scope="browser" - 什么 key
- 有效到什么时候
2. 默认有效期不是永久
_get_trusted_device_age() 默认是 90 天,还支持通过参数配置。
所以“记住设备”本质上是:
在有限时间内,允许这台浏览器替代一次二次验证。
不是永久豁免,更不是取消 2FA。
3. 改密码会撤销所有受信设备
change_password() 里会主动调用 _revoke_all_devices()。
这非常关键,因为密码变更往往意味着:
- 用户怀疑账号泄露
- 或管理员执行了安全收口
如果这时旧受信设备还继续有效,2FA 的安全性会被打折扣。Odoo 在这里做的是“密码边界变化 → 设备信任也一起失效”。
为什么启用 / 禁用 TOTP 后还要刷新 session token
无论 _totp_try_setting() 还是 action_totp_disable(),都在修改 TOTP 状态后刷新了当前 session token。
这一步很容易被忽视,但非常重要。
因为 Odoo 的会话令牌并不只是“你登录过”,还隐含了当前认证状态。如果认证要素变了:
- 之前没有 TOTP,现在有了
- 之前有 TOTP,现在被关掉了
那旧会话如果继续原样使用,就可能和最新安全状态不一致。
所以这里的本质不是“别把用户踢下线”,而是:
在不打断当前用户体验的前提下,把会话绑定到新的认证事实。
新手最容易误解的 4 件事
1. 误以为 TOTP 启用就是“保存 secret”
其实必须先用实际验证码证明 secret 已经可用。
2. 误以为验证码正确就行
源码还防了重放,同一个已成功使用的 counter 不能再用。
3. 误以为“记住设备”就是跳过安全
它只是给特定浏览器发一把有期限、可撤销的信任凭据。
4. 误以为改密码和 TOTP 设备无关
Odoo 恰恰把两者绑在一起,改密码会撤销全部受信设备。
实战排错顺序
如果你碰到“明明码没错却过不了”或“勾了记住设备还是反复要验证码”,建议按这个顺序查:
- 先看用户是否真的已写入
totp_secret - 确认
totp_last_counter是否导致旧验证码被判重放 - 检查
auth.totp.rate.limit.log是否已触发限流 - 看浏览器是否真的拿到了
td_idcookie - 确认设备有效期参数有没有被改成异常值
- 如果刚改过密码,先别怀疑 bug,旧受信设备本来就会失效
一句话记忆
Odoo TOTP 的重点不是“再输一次验证码”,而是用启用验证、防重放、限流、受信设备和会话刷新,把二次认证变成一条真正可控的安全链路。
DISCUSSION
评论区