先说结论
Odoo 的 auth_signup 不是一个“漂亮的注册页面模块”,而是一套把外部点击动作落回到 partner / user 生命周期的账户认领机制。
从 /home/ubuntu/odoo-temp/addons/auth_signup/models/res_users.py 和 controllers/main.py 来看,最关键的不是页面长什么样,而是下面这几件事:
- signup token 实际上是先指向
res.partner,再决定要不要落成res.users。 - 同一个 token 机制,同时服务于“首次认领账号”和“重置已有账号密码”。
- 未受邀用户能不能自己开户,取决于
invitation_scope是b2b还是b2c。 - 首次开户时复制哪一个 Template User,决定了新账号的默认角色轮廓。
所以,auth_signup 的本质不是“开放一个注册入口”,而是定义 Odoo 账户到底如何被认领、激活和找回。
为什么 Odoo 先找 partner,而不是直接找 user
signup(values, token=None) 这段逻辑非常能说明问题。
如果传入 token,Odoo 会先调用:
res.partner._signup_retrieve_partner(token, check_validity=True)
也就是说,token 首先对应的是一个 partner 语义,而不是简单的“某个用户名字符串”。
这有两个直接后果:
1. 邀请注册是“认领业务主体”
在很多业务场景里,partner 早就存在了:
- 客户联系人已经在系统里;
- 员工档案已录入;
- 供应商联系人已建档;
- 门户用户将来要看的订单 / 发票 / 项目,也都挂在 partner 上。
所以邀请链接不是“凭空创建一个陌生账户”,而是在说:
你现在来认领这条已经存在的业务身份。
2. token 是否落成新用户,要看 partner 下面有没有 user
源码里接着判断 partner.user_ids。
- 如果已有 user:这是“设密 / 重置 / 激活已有账号”路径;
- 如果还没有 user:这是“第一次从 partner 落成 user”路径。
这条分叉非常关键,因为它解释了为什么你看到的页面很像,但背后并不是同一件事。
邀请、注册、重置为什么“共用机制但不是一回事”
很多团队上线时会把这三件事混在一起:
- 邀请用户入驻;
- 公开开放注册;
- 忘记密码找回。
Odoo 确实复用了很多相同基础设施:
- 都可能生成 signup token;
- 都会发邮件;
- 都可能走
do_signup(); - 最终都可能写入密码。
但它们的业务前提完全不同。
邀请注册
管理员先创建 / 选定主体,然后发链接。重点是:谁有资格认领这个身份。
开放注册
访客自己发起。重点是:系统是否允许未受邀开户。
忘记密码
账号已存在,只是重建密码。重点是:邮箱所有权与找回安全。
源码把它们放进一套框架里处理,但并没有把三者混成同一个概念。实施时如果你也把它们混了,后面权限与安全边界一定会乱。
invitation_scope 才是真正的“开放程度开关”
_get_signup_invitation_scope() 读取的是:
auth_signup.invitation_scope
默认值是 b2b。
在 _signup_create_user() 里,若 values 里没有 partner_id,Odoo 会认为这是未受邀自注册;这时如果 scope 不是 b2c,就直接抛:
Signup is not allowed for uninvited users
这条边界非常清晰:
b2b:默认偏邀请制;b2c:允许公开注册。
所以很多人以为“装了 auth_signup 就自动开放注册”,其实不对。模块提供了能力,但是否开放,要靠配置明确授权。
Template User 为什么是整条链路里最危险也最容易忽略的角色
当 partner 还没有 user 时,Odoo 会走 _create_user_from_template()。
这里会读取:
base.template_portal_user_id
然后以这个模板用户为蓝本执行 copy(values)。
这说明首次开户不是“生成一个空白用户”,而是复制一个预定义角色轮廓,再覆盖 login / partner_id / name / email 等关键字段。
这个设计很实用,因为它让 Odoo 可以快速给新用户带上:
- 基础组权限;
- 默认公司;
- 默认语言或其它继承值。
但风险也恰恰在这里:
如果 Template User 选错,所有新用户的默认安全边界都会一起偏掉
常见问题包括:
- 模板用户权限太大,新开户用户天然越权;
- 模板太“空”,新用户首登后一堆页面打不开;
- 多公司字段继承混乱,导致一开始就看到不该看的公司;
- 门户与内部用户模板混用,造成角色错位。
所以 auth_signup 里最不该被当成“系统默认值随便用”的,其实就是 Template User。
为什么 token 用完后要立刻失效
在 signup(token=...) 的 token 路径里,Odoo 会先执行:
partner.write({'signup_type': False})
这一步很值得注意。它意味着 token 不是长期凭证,而是一次性认领凭证。
安全上这非常合理,因为如果链接被长期复用:
- 邀请链接会变成半永久后门;
- 重置密码链接可能被旧邮件再次打开;
- 首次设密完成后,链接残留就会增加劫持风险。
所以 Odoo 的意图很明确:
token 用来打开一次“账号认领窗口”,不是长期替代登录。
为什么现有用户和新用户写入的字段不一样
源码对这两种情况做了非常明确的分流。
partner 已有 user:更像“更新现有账号”
如果 partner_user 已存在,Odoo 会主动把 login、name 从提交值里弹掉,不允许随便覆盖这些关键身份字段,然后只写允许更新的值,比如新密码等。
这说明邀请 / 重置并不是“用户拿到链接后想改什么都行”,而是:
- 身份主语已经确定;
- 链接只是允许你完成激活或重建口令。
partner 还没有 user:才是真正的“首次落库”
这时 Odoo 会把 name、partner_id、email、company 信息等补齐,再调用模板复制逻辑。
也就是说,新建用户和更新旧用户在源码里从来不是一条含糊的流程,而是两类完全不同的对象转换。
reset password 为什么不是单独一套系统
reset_password(login) 的逻辑看似简单,但非常典型:
- 先按 login 查;
- 不行再按 email 查;
- 没找到报错;
- 找到多条也报错;
- 只有唯一命中,才继续
action_reset_password()。
而真正发邮件时,_action_reset_password(signup_type="reset") 仍然会走 partner.signup_prepare(...) 生成 token,再发带链接的邮件。
这说明“忘记密码”本质上也是一种受控的账号再认领。
区别只是:
- 邀请设密是为了首次激活;
- reset 是为了恢复已有账户控制权。
框架一样,语义不同。
控制器层最重要的边界:页面公开,不代表账号一定公开
/web/signup 和 /web/reset_password 都是 public route,但能不能真正完成动作,取决于 qcontext 里的配置和 token 状态。
例如:
- 没有 token 且未开启
signup_enabled,/web/signup直接 404; - 没有 token 且未开启
reset_password_enabled,/web/reset_password直接 404; - token 无效,则页面会标记
invalid_token。
这说明 Odoo 的思路不是“把入口藏起来”,而是:
- 页面可访问;
- 但真正的业务动作必须满足配置与 token 有效性。
这是一种很典型的平台式安全设计:公开路由 ≠ 公开权限。
实施里最值得提前做的 5 个决定
1. 你的系统到底是邀请制还是开放注册
不要模糊处理。
- ToB / 内部系统:大多数时候应该留在
b2b; - 真正面向陌生访客自助入驻:才考虑
b2c。
2. Template User 到底是谁
这不是一个 UI 小配置,而是新账户默认安全轮廓。
3. 邮件是否可靠可达
如果出站邮件不稳定,用户视角里就是“系统没反应”。邀请 / 重置链路会显得极不可信。
4. login 的唯一口径是什么
是邮箱、员工编号、还是客户编号?如果口径不统一,reset_password 的唯一命中就可能出问题。
5. token 的有效期和业务节奏是否匹配
链接太短,用户来不及操作;太长,又会增加残留风险。你要让安全与实际使用场景对得上。
最后一句
很多人以为 auth_signup 的关键是“注册表单”。
其实从源码看,它真正管理的是:
- 谁可以认领一个 partner 身份;
- 认领时是新建 user 还是更新旧 user;
- 默认复制什么角色模板;
- 何时用 token 打开一次性设密窗口;
- 何时只允许受邀,何时允许陌生人自己开户。
所以,Odoo 的邀请注册从来不是“发封邮件就完了”,而是一条标准化的身份认领链路。把这条链路看懂了,你才知道系统到底是在开户、激活,还是恢复控制权。
DISCUSSION
评论区