...
Back

一个账号、三个平台、六种登录方式

把 web、iOS 和 Android 的身份打通不是认证问题。它是「什么算同一个人」的定义问题,而你必须在写任何代码之前回答它。

一个账号、三个平台、六种登录方式

一个账号、三个平台、六种登录方式 🔑

我们上线了 web、iOS 和 Android 的身份打通 —— 一个账号,数据到处一致,无论你在哪个平台注册、用哪种方式登录。听起来像接管道的活。它主要是个定义问题,而管道只有在定义敲定之后才变容易。


必须先回答的那个问题

什么让两次登录算作同一个人?

所有实现细节都由它推导而来,而搞错它在两个方向上都很贵:合并得太积极,你会把一个用户的数据交给另一个;合并得太保守,一个真实的人会拥有三个账号,外加一张"我的历史记录不见了"的工单。

候选项,以及各自的不完美之处:

邮箱。 显而易见的答案,单独使用时很危险。有些身份提供方允许用户用中转地址隐藏真实邮箱,于是同一个人在不同平台上呈现出不同地址。另一些则根本不验证地址 —— 而拿一个未验证的邮箱当合并键,意味着任何敲对你地址的人都能继承你的账号。

手机号。 在你发验证码的那一刻就天然完成了验证,这是它的强项。但号码会被运营商回收,所以在足够长的时间尺度上,手机号标识的是一份套餐,不是一个人。

提供方的 subject 标识。 在单个提供方内部稳定且可信。跨提供方就没用了 —— 同一个人用两个不同社交账号登录就是两个 subject,而两份令牌里都没有任何东西说它们有关系。

可行的模型是:用户是一个持久的内部标识,而每一种登录方式都是链接到它的一份凭据。 永远不要把任何外部标识当成用户本身;把它当成指向用户的一把钥匙。


我们定下的规则

只在经过验证、且非中转的标识上合并。 已验证邮箱或已验证手机。中转地址只链接到它被签发给的那个账号,别的都不链。未验证的地址什么都不链。

链接需要双边证明。 给一个已有账号添加第二种登录方式,意味着既处于登录状态,证明了新凭据。仅仅因为新方式碰巧带了一个匹配的邮箱就接受它,那是多绕了几步的账号劫持。

绝不自动合并两个都有数据的账号。 这在实践中不可逆。如果一个用户真的有两个账号,那是一条需要明确确认"两边的历史各自会怎样"的客服流程 —— 而不是一个登录处理函数在凌晨三点替人做的决定。

一个规范的用户标识,到处都用它。 每个平台、每张表、每个分析事件。一旦某个设备存下了它自己的"这是谁"、而且可能与服务端不一致,你就有了一类只在报告它的那个用户身上复现的 bug。


平台差异真正咬人的地方

令牌生命周期因平台而异,而刷新不是免费的。 一个在每次操作前强制刷新令牌的移动端 app,会在网络糟糕时挂上刷新所需的那么久 —— 而对一个在某些地区慢或被阻断的身份端点来说,这可能是几分钟。缓存凭据、后台刷新,并给任何挡在用户操作前面的刷新设一个上限。

用原生提供方登录需要按平台分别配置。 同一个提供方在 web 和原生上需要不同的客户端标识和不同的回调 URL,而一个"在某平台启用了但在另一平台配错了"的提供方,产生的故障看起来像用户操作失误。这份审计很枯燥,而它就是全部工作:对每一个提供方、每一个平台,它注册了吗、启用了吗、指向正确的回调 URL 了吗?

每一个提供注册的平台上都必须能注销账号。 商店有此要求,而在合规之外,一个用户无法在其创建它的设备上删除的账号,会永远是客服负担。


我们会换一种做法的地方

从第一个提交起就把身份建模成用户 + 已链接凭据,哪怕当时只有一种登录方式、这种拆分看起来像过度设计。事后再补意味着要在活跃账号上做迁移,而其中每一行有歧义的记录,都是一个你即将搬动其数据的真实的人。

还有:把合并规则写下来 —— 写在代码里、写成注释、用大白话。我们的归结为一句话:按已验证邮箱或已验证手机链接;绝不合并两个都持有数据的账号。 这个领域里的每一次事故,都来自某人从零开始推理这条规则,然后得出了一个与前人略有不同的结论。