先说结论
Odoo 的 Passkey 不是“把密码框换成生物识别图标”这么简单。
从 /home/ubuntu/odoo-temp/addons/auth_passkey/models/res_users.py 和 controllers/main.py 能看出,官方真正关心的是四件事:
- 浏览器拿来的 WebAuthn 凭证到底对应哪个用户
- 当前登录是在做“首次发现用户”还是“验证已登录用户”
- 签名计数
sign_count能不能帮助识别可疑重放或克隆设备 - 用户新增或删除 Passkey 后,旧会话是否还应该继续信任
所以最短结论是:
Passkey 在 Odoo 里不是一个 UI 功能,而是一条完整的认证链路。
为什么 Odoo 要先“通过凭证找登录名”
_login() 的一个关键动作是:
- 如果凭证类型是
webauthn - 先从
auth_passkey_key里按credential_identifier找到对应用户 - 再把
credential['login']补进去,继续走后面的登录流程
这一步很重要,因为 Passkey 登录和传统用户名密码登录的起点不同。
传统登录通常是:
- 你先说“我是谁”
- 系统再验证“你是不是这个人”
Passkey 更像:
- 你先拿出一个浏览器 / 设备签出来的凭证
- 系统反过来判断“这个凭证属于谁”
这就是为什么源码里不是先依赖用户输入账号,而是先拿凭证 ID 去查用户。
这对业务理解意味着什么
很多团队以为 Passkey 的价值只在“免输密码”。
其实从系统实现看,它的价值更接近:
把“声明身份”从手填账号,改成浏览器提交的受保护凭证。
也因此,真正关键的是凭证注册、凭证归属和后续验证,而不是页面上那一个按钮。
为什么新增 Passkey 前要走 identity check
在 action_create_passkey() 上,Odoo 明确用了 @check_identity。
这说明系统并不把“当前会话已经登录”视为足够安全的前提,而是把“新增一个长期可用的登录凭证”当成高风险动作。
通俗说就是:
- 普通浏览页面,当前登录态可能够用
- 但如果你要给账号再绑一把新钥匙,系统希望你再次证明自己
这背后的安全思想非常朴素:
登录态只证明你现在像是这个人;身份确认则是要求你在高敏感操作前再次证明你真的是这个人。
这一点和很多平台在改密码、关 2FA、绑定新设备时要求二次确认,本质上一样。
_check_credentials() 到底做了什么
当凭证类型是 webauthn 时,Odoo 的核心校验链是:
- 解析浏览器传来的
webauthn_response - 根据当前用户和
credential_identifier找到那条 Passkey 记录 - 调用
_verify_auth()验证签名 - 比对并更新
sign_count - 返回认证结果,并明确写上:
-
auth_method: 'passkey'-mfa: 'skip'
这里最值得注意的是最后一项:mfa: 'skip'。
这不等于 Odoo 不重视 MFA
相反,这表示:
- Odoo 把 Passkey 视为已经足够强的认证手段
- 它不是“先过一层弱认证,再补一层 MFA”
- 而是直接把 Passkey 当成可以完成强认证的入口
这也是为什么 Passkey 的产品定位经常被称为“密码替代”,而不是“密码附件”。
sign_count 为什么是这条链路里最容易被忽略的细节
源码里验证成功后,会把新的 sign_count 写回去。
很多人第一次看会疑惑:
- 为什么每次登录还要更新一个计数器?
因为在 WebAuthn 模型里,这个计数器有助于发现异常情况。
你可以把它理解成:
- 设备每次成功用这把钥匙签名,都会让计数向前走
- 如果系统发现计数异常,就会怀疑是不是出现了凭证复制、设备克隆或认证上下文异常
它当然不是万能防线,但它属于那种“平时看不见,出事时很值钱”的追踪字段。
很多安全设计都不是靠一个大而全的万能判断,而是靠几个小而稳的边界共同收口。sign_count 就属于这一类。
为什么“删除 / 新增 Passkey”会影响旧会话可信度
_get_session_token_fields() 和 _get_session_token_query_params() 把 auth_passkey_key_ids 纳入了 session token 的计算依赖。
这意味着:
- 用户账号下的 Passkey 集合变了
- 会话签名依赖也会变
- 原本建立在旧凭证集合之上的 session token 会失去原先的稳定前提
这一步非常关键,因为它解决的是一个常见盲区:
如果账号的认证因子集合都变了,为什么旧会话还能继续被原样信任?
Odoo 的回答是:
- 不应该。
也就是说,Passkey 不只是登录入口层面的增强,它还被纳入了会话有效性模型。
这比“只要能登录就行”要成熟得多。
/auth/passkey/start-auth 这条路由说明了什么
控制器里暴露了 /auth/passkey/start-auth,返回 _start_auth() 生成的认证参数。
这反映出一个很典型的 WebAuthn 结构:
- 浏览器不能凭空发起一次可信认证
- 服务端必须先生成挑战参数
- 浏览器 / 系统安全模块再基于挑战完成签名
- 服务端最后验回来的响应
所以 Passkey 从来都不是“前端自己搞定”的功能。
它一定是:
- 服务端挑战
- 客户端签名
- 服务端验证
三段式闭环。
如果团队只盯着前端按钮和浏览器弹窗,往往就会低估后端这段挑战—验证链的安全价值。
新手最容易误解的 4 件事
1. 误以为 Passkey 只是“无密码体验优化”
体验当然更好,但源码重点明显在认证强度和会话边界,不只是少输一次密码。
2. 误以为已登录用户新增 Passkey 不算敏感操作
@check_identity 已经明确告诉你:这就是敏感操作。
3. 误以为 Passkey 登录只需要前端适配
没有服务端挑战、凭证归属查询、签名验证和 session 失效设计,这件事根本不完整。
4. 误以为 Passkey 上线后,账号治理逻辑不用变
恰恰相反。
你需要重新思考:
- 凭证丢失怎么办
- 用户换设备怎么办
- 管理员撤销凭证后旧会话怎么办
- Portal 用户是否也允许同等能力
这些都是平台治理问题,不只是登录页问题。
实战排错顺序
如果用户反馈“Passkey 能注册但登录异常”或“删掉凭证后会话表现奇怪”,建议按这个顺序查:
- 先确认
credential_identifier是否能唯一找到那条 Passkey 记录 - 再看当前操作是登录认证,还是已登录状态下新增凭证
- 检查
_verify_auth()是否因签名验证失败抛出异常 - 确认
sign_count是否出现异常回退或不更新 - 检查用户增删 Passkey 后,相关 session token 是否按预期失效
这套顺序的价值在于:
- 先查身份归属
- 再查签名校验
- 最后查会话连带影响
不要一上来只盯浏览器弹窗。
一句话记忆
Odoo 的 Passkey 不是“换掉密码框”,而是把用户发现、身份确认、签名验证、计数追踪和会话失效绑成了一条完整认证链。
DISCUSSION
评论区