TOTP

Odoo TOTP 为什么不只是“输入一次 6 位码”而已:启用流程、限流、防重放与受信设备边界讲透

很多人把 Odoo 的 TOTP 理解成登录页多一步验证码,但源码真正做的是启用校验、防重放、会话令牌刷新、受信设备与限流的整套安全收口。本文把这条链路讲透。

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

先说结论

Odoo 的 TOTP 不是“用户绑定一个二维码,以后多输一位验证码”这么简单。

/home/ubuntu/odoo-temp/addons/auth_totp/models/res_users.pymodels/auth_totp.pycontrollers/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 恰恰把两者绑在一起,改密码会撤销全部受信设备。


实战排错顺序

如果你碰到“明明码没错却过不了”或“勾了记住设备还是反复要验证码”,建议按这个顺序查:

  1. 先看用户是否真的已写入 totp_secret
  2. 确认 totp_last_counter 是否导致旧验证码被判重放
  3. 检查 auth.totp.rate.limit.log 是否已触发限流
  4. 看浏览器是否真的拿到了 td_id cookie
  5. 确认设备有效期参数有没有被改成异常值
  6. 如果刚改过密码,先别怀疑 bug,旧受信设备本来就会失效

一句话记忆

Odoo TOTP 的重点不是“再输一次验证码”,而是用启用验证、防重放、限流、受信设备和会话刷新,把二次认证变成一条真正可控的安全链路。

DISCUSSION

评论区

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