看到“邮箱验证码二次验证”,很多人会把它想得很简单:
发一封邮件,用户把 6 位数填回去。
但 Odoo 的 auth_totp_mail 不是一个普通验证码模块。
它做的是一套完整的邮件版 TOTP / 重认证机制,并且要和 auth_timeout 的会话重验逻辑协作。
这意味着它要同时解决四件事:
- 验证码怎么生成,才不会被轻易复用?
- 一小时内的 code 如何和会话状态绑定?
- 邮件发送和验证码校验如何限流?
- 在 session 需要重认证时,怎样和 password / TOTP / passkey 共存?
1. 邮箱验证码不是“随机数”,而是和用户状态绑定的 HMAC + HOTP
关键方法在 addons/auth_totp_mail/models/res_users.py:
_get_totp_mail_key()_get_totp_mail_code()_check_credentials()
这里的思路不是简单 random.randint(),而是先构造一个和用户状态绑定的 key:
- 用户 id
- login
- login_date
这些值通过 HMAC 组合在一起。
这带来一个很重要的效果:
只要用户的登录态发生变化,验证码的底座就会变化。
也就是说,这不是“发出去的一串码可以长期存在”的设计。 它天然带着会话边界和身份状态。
然后 _get_totp_mail_code() 用这个 key 生成一个 HOTP,并把 counter 设为“当前时间按小时切分的序号”:
counter = int(datetime.timestamp(now) / 3600)expiration = timedelta(seconds=3600)
所以它本质上是小时级 OTP,不是按 30 秒轮转的传统 TOTP。
这也是为什么用户看到的验证码会有“1 小时有效”的语义。
2. 发送邮件和校验验证码,都有自己的限流
Odoo 这里做了两层限流:
send_emailcode_check
在 TOTP_RATE_LIMITS 里,两者默认都是 5 / 3600。
这意味着:
- 你不能无限触发发码邮件
- 你也不能无限试验证码
限流日志存在 auth.totp.rate.limit.log,并且是按:
- user_id
- limit_type
- 时间窗口
来统计的。
成功验证后,系统会清掉对应限流日志:
- 验证成功后 purge
code_check - 发送成功验证后还会 purge
send_email
这说明 Odoo 的意图不是“让限流成为永久惩罚”,而是把它当作防爆破和防刷邮件的防线。
3. /web/login/totp 不是普通表单,而是“先发码,再等人输入”的入口
在 addons/auth_totp_mail/controllers/home.py 里,web_totp() 会先调用父类逻辑。
只有当:
- 响应是正常页面
- 当前用户的
_mfa_type()是totp_mail
才会继续执行发送邮件。
也就是说,邮件发码不是提前乱发,而是绑定在“用户刚进入二次验证页”这个动作上。
而且发送动作被包在 savepoint() 里:
- 如果发邮件失败,只会把错误放回页面上下文
- 不会把整个认证流程直接炸掉
这很符合 Odoo 的风格:
表单先打开,失败信息先展示,用户再决定重试。
这比“接口一失败就抛异常退出”更适合真实登录场景。
4. auth_timeout 把它放进了“重认证”流程,而不是独立登录流程
真正让这个模块完整起来的,是 auth_timeout/models/ir_http.py。
它的 _must_check_identity() 会判断两种超时:
lock_timeout:会话总寿命到了,必须重新登录lock_timeout_inactivity:用户闲置太久,需要重新确认身份
一旦需要重认证,_check_identity() 会根据当前可用的 auth methods 走流程。
注意这里它会把 totp 和 totp_mail 统一当作可选 second factor 之一处理:
- 先取 auth methods
- 如果已有第一因素,就把同一种方法从候选里去掉
- 再用
request.session["identity-check-1fa"]避免 1FA/2FA 复用混乱
这说明 totp_mail 不是单独的“邮箱登录”,而是重认证链路中的一种 MFA 形式。
如果用户先前已经用过 password,再来验证邮箱码,本质上是在完成“重新证明你还是你”的步骤。
5. totp_mail 和普通 TOTP 的差别,不只是“介质不同”
它和手机 TOTP 的差别至少有三层:
-
生成方式不同 - 手机 TOTP 更像稳定密钥 + 时间窗口 -
totp_mail把login_date也纳入 key -
时间窗口不同 - 手机 TOTP 常见是 30 秒 -
totp_mail是小时窗口 -
用户体验不同 - 手机 TOTP 适合高频验证 -
totp_mail更适合“低频、重认证、没有认证器 App”的场景
所以它不是“弱化版 TOTP”,而是为邮件通道定制的 MFA 方案。
6. 这个模块最容易被误解的点
最容易误解的地方有两个:
- 第一,以为它只是“发一个一次性验证码邮件”
- 第二,以为它是登录时的可选校验,和会话重认证无关
实际上,auth_totp_mail 是和 auth_timeout 深度耦合的。
它既能作为初次登录的 MFA,也能作为会话过期后的身份确认手段。
如果你在排查问题,建议按这个顺序看:
- 用户是否真的属于
totp_mail这类 MFA - 邮件是否发送成功
- 验证码是否还在 1 小时窗口内
- 发送/校验限流是否触发
auth_timeout是否正在要求你进入重认证流程
结论
Odoo 的邮箱验证码不是“发一封验证码邮件”这么简单。
它是一个把:
- 用户状态
- 时间窗口
- 限流机制
- 会话重认证
全部串起来的 MFA 方案。
也正因为这样,它看上去很轻,实际上边界一点都不轻。
DISCUSSION
评论区