安全与供应链开发工具

登录时多出来的那一步:MFA 的设计逻辑与 Passkey 的模型变更

你刚输完密码,页面没直接进去,弹出一个输入框,要你填六位数字。数字在一封邮件里,邮件在 spam 文件夹里,你切过去翻了两页才找到,复制粘贴回来,已经超时了。刷新重来,换了一个渠道——短信。这次快一些,通知栏弹出四位数字,系统自己填进去了。登录成功。

这两个场景做的事本质上一样:在密码验证通过之后,网页多问了你一个问题,等你答对了才放行。但体验差异大到可以问一句:设计层面,这两者在做什么?以及后面那个看起来更顺滑的东西,会不会在安全上付出了代价?

密码为什么不够

密码是一个共享的秘密。你注册账号时设一个密码,网站存一份哈希值。登录时你把密码传过去,网站验证它和哈希值匹不匹配。

这个模型有三个脆弱点。

第一,数据库泄漏。网站被拖库后,密码哈希落到攻击者手里。哈希本身不能直接用来登录,但当密码足够弱时,哈希可以被反向计算出来。对于那些在多个网站用同一个密码的人,一个网站的泄漏会蔓延到其他网站。

第二,钓鱼。攻击者做一个和真实登录页一模一样的页面,诱导你输入密码。你输入的是正确密码,但它被发到了错误的地方。

第三,重用。大多数人在记忆几十个不同密码这件事上做得不好,所以会复用一个基础的秘密,或者在它上面加很小的变体。

这三个脆弱点背后是同一件事。密码的工作方式是每次登录都把同一个秘密告诉网站。这个”告诉”的动作,本身就会创造泄漏的机会。

网站替你存着这个秘密,所以存在存漏的可能。你把秘密交给网站,存在交给冒牌网站的可能。而因为不同网站要的是同一个秘密,一个地方的泄漏会蔓延到所有地方。

MFA 想达到什么

多因素验证的出发点是一个简单的逻辑:如果密码这个秘密可能被窃取,就让窃取本身变得没用。即便攻击者拿到了密码,也不能登录,只要他们没有第二样东西。

这里的”第二样东西”有标准分类。你知道的:密码、PIN。你持有的:手机、硬件密钥。你自己:指纹、面容。MFA 的基本要求是至少选用来自两个不同分类的因素。登录要求一个你知道的加上一个你持有的,这样单点泄漏不足以通过认证。

所以多因素验证在解决的不只是”更安全”这个模糊目标,而是三个具体问题。让被盗的密码失效。让一次性的攻击尝试无法被重放。让攻击者攻破一个因素时还需要攻破第二个来自不同分类的因素。

这三件事定义了后面每一种方案的设计取舍。

邮件验证码

邮件验证码是最早普及的 MFA 之一。设计逻辑直接:每个用户注册时都填了邮箱,邮箱本身已经是账号恢复的信道。在密码之外再发一封带有六位数字的邮件,产品改动最小。

从设计端看,这个方案利用的是一个已经存在的身份锚点。网站可以假设:能访问这个邮箱的人,大概率就是这个账号的拥有者。不需要用户安装新软件,不需要换 SIM 卡,不需要学习新概念。一封邮件,是产品逻辑上最省力的第二步验证。

但邮件作为验证信道有三个结构性问题。

可靠送达不是默认的。垃圾过滤器、发件人信誉、邮件服务提供商的节流策略,都会导致邮件延迟或者干脆不到。这不是偶然故障。邮件协议被设计成一个异步的、允许重试的、不需要即时送达的系统。而 MFA 的需求是相反的:用户点击”发送验证码”后的 30 秒内,它必须出现在收件箱里。这两套时间预期不是同一个数量级的。

攻击者也可能在你的邮箱里。如果攻击者已经获取了邮箱账户的密码,邮件 MFA 的第二道防线会直接消失。因为验证码发到的那个信箱,对方已经能进去了。密码重用的后果在这里不是只影响一个网站,它可以把 MFA 本身也拖垮。

体验问题同样不小。邮件验证码打断登录流程的方式很生硬。你在浏览器里,验证码在邮件客户端里。你得停下来,切换到另一个应用或标签页,找到那封邮件,在一堆发件人和标题里定位到正确的那封,复制验证码,切回去,粘贴。有些邮件客户端支持 autofill,但这并不普遍,尤其在 webmail 和桌面端。

短信验证码

短信验证码在设计上和邮件是同构的:手机号也是一个身份锚点,验证码通过运营商网络送过来,你输入它。

但体验有一个本质差别:大多数智能手机在接收到验证码短信后,系统级的 autofill 直接把它填进输入框。完成登录的过程中,用户不需要离开当前的 app 或浏览器标签页。相比邮件的切换、搜索、复制、粘贴四步,短信的交互路径短得多。收到通知,看到数字,自动填入。

这种体验提升不是因为短信协议更先进。它来自操作系统对”短信里的验证码”这个模式做了专项优化,iOS 和 Android 都内置了解析和自动填充。反过来,邮件内容的结构远比短信灵活,同一个优化在工程上更难做。

所以在用户体验这个维度上,短信比邮件好,好的是系统集成程度,不是安全模型。它的安全模型和邮件属于同一类:一个外部信道发送一次性验证码。邮件可能因为垃圾过滤器延迟,短信可能因为运营商路由延迟。两者的共同弱点是验证码在传输过程中以明文形式经过第三方基础设施。

SIM 卡克隆和 SS7 信令网络攻击是短信验证码被讨论最多的两个安全缺陷。不过这些攻击的门槛远高于密码重用和钓鱼。对绝大多数普通用户来说,短信验证码的威胁模型更直接:你的手机号码会不会被运营商或社会工程攻击拿过去?

所以在安全性和体验之间,短信验证码的位置大体重叠:比纯密码强不少,比邮件体验好不少,但在定向攻击面前和邮件一样脆弱。

TOTP 验证器

TOTP 做了一件邮件和短信都不做的事:它不发送验证码。

TOTP 全称是 Time-based One-Time Password。核心机制:注册时,网站和你的验证器之间共享一个密钥。这个密钥通过一个二维码完成交换,不经过短信或邮件。之后,你的验证器用这个密钥加上当前的时间窗口,在本地计算出一个六位数字。网站用同样的密钥和同样的时间窗口独立计算一遍,数字对上了就通过。

关键变化是验证码不再经过任何第三方信道。没有短信运营商参与,没有邮件服务器参与。验证器直接在本地算出来,不依赖信号、网络连接、发件人投递质量。

从安全设计的角度看,这一步削减的是信息传输链路上的攻击面。邮件和短信的问题不是验证码本身不安全,是送验证码的那条路径上有太多环节不在你的控制范围内。TOTP 把信道整个换掉,替换成双方在两端独立计算同一个结果。

代价也在。第一,onboarding:你需要安装验证器 app,扫描二维码,理解”不要删这个凭证”这件事。第二,设备丢失:手机丢了或换了新手机,原来存在验证器里的凭证能不能恢复,取决于验证器是否支持备份。Google Authenticator 长期不支持云备份,出现过不少人换手机之后挨个联系网站客服重新绑定的情况。Authy 和后来更新的 Google Authenticator 支持加密备份后,这个问题有所缓解。

Passkey

前面的方案都在密码之上叠加第二道验证。Passkey 不叠加,它把密码替换成了非对称密钥。注册时设备生成一对公私钥,公钥发给网站,私钥留在本地。登录时网站发随机挑战,设备签名返回,网站用公钥验证。

原理和 SSH key 登录一样。对公私钥不熟的读者,一个粗略类比是质因数分解:两个大质数相乘很快,把乘积拆回原来的两个质数极难。公私钥的安全性就建立在类似性质的单向函数上。

密码的三个脆弱点因此全部消失。网站只存公钥,拖库了没用。签名绑定了域名,钓鱼页面签不出合法凭证。没有密码,重用不成立。

和 SSH 私钥文件的一个区别:Passkey 的私钥在主流系统里不可导出。网页只能请求设备”帮我签一下这些数据”,设备返回结果,私钥不出安全存储区。即使登录页面有恶意脚本,也偷不走设备上的私钥。

在设计脉络里看,Passkey 做的不是在 MFA 上再加一层。它把认证从”共享秘密”换成了”持有密钥但不需要把密钥发出去”。这个变化和前面三种方案不在同一条延长线上。

从补丁到模型变更

把四种方案铺开,能看到一个清晰的演化方向:每一步都在移除一个关于信任的假设。

密码方案假设用户不会重用密码、网站不会泄漏哈希、用户不会点进钓鱼页面。三个假设都不成立。

邮件 MFA 在前一个基础上新增了假设:邮件服务器是安全的,验证码能在规定时间内抵达。这个假设部分成立,多数时候邮件发得出去。但它同时也假设邮件账户没被攻破、垃圾过滤器不会误判、用户能在几十封未读邮件里定位验证码。每一步都可能断。

短信 MFA 换了信道,假设也跟着换:运营商网络是安全的,手机号码不会被 SIM swap 拿走,短信能即时送达。相比邮件,送达可靠性提升了很多,但信令层面的攻击仍然在。

TOTP 直接撤掉了对信道的依赖。它不再假设”外部服务会把验证码送到你手里”,改为假设”设备时间是准确的,本地存储的共享密钥没有泄漏”。攻击面收缩到了本地设备。

Passkey 撤掉了最后一个假设:不再需要共享秘密。服务端数据库不再存任何能用来重构登录凭证的东西。攻击者拿不到私钥,因为私钥不经过服务端也不参与传输。攻击者只能在本地设备层面尝试冒充你,这需要同时攻破设备解锁和认证器的安全边界。

这条演化线的模式不是”越来越安全”。大多数方案在它被设计的年代,安全水平相对当时的威胁是足够的。模式是:每一代都收缩了一个前代依赖的隐式假设。从”用户的密码习惯都好”,到”邮件总能准时到”,到”本地存储不会被攻破”,再到”不需要共享任何秘密”。

所以 Passkey 不只是一个更好的 MFA。它在身份验证的设计路径上换了一个方向:不再在”增加验证因素”这条延长线上往下走,而是把需要被验证的那个东西从共享秘密换成了非对称密钥。

实用判断

对日常使用来说,分层处理就够了。

高价值账号,Google、Apple、GitHub、银行、密码管理器、域名注册商、Cloudflare 这一类,优先上 Passkey 加至少一个备用恢复方式。准备两把硬件安全密钥,一把日常用,一把锁在安全的地方做备份,是比依赖单一同步机制更稳健的方案。

普通网站用平台同步的 Passkey,iCloud Keychain、Google Password Manager、1Password 都行,足够。它比任何你记忆中的密码安全,比短信和邮件 MFA 快,比 TOTP 的 onboarding 摩擦低。

如果某个网站还不支持 Passkey,用 TOTP 验证器配合密码管理器自动填充。这个组合的安全水平不如 Passkey,但远好于短信和邮件 MFA。不要在关键账号上只用邮件作为第二因素。

一条容易被忽略的提醒:不要只设一个 Passkey 就把所有恢复方式删掉。Passkey 的安全性依赖你能持续访问存放私钥的设备或账户。如果你用 iCloud Keychain,Apple ID 的安全性就更关键。确保 Apple ID 自身也开启了强 2FA 或 Passkey,并且有可用的恢复联系人或者恢复密钥。如果你用 1Password,Emergency Kit 和主密码需要在安全的地方有备份。身份验证越集中到少数设备和账户,这些设备和账户就越需要冗余保护。

开发者怎么选

接入 MFA 通常走两条路:用第三方认证服务,或者自己拼。走第三方是最常见的做法,好处是 SDK 里已经把多种 MFA 方式之间的回退逻辑、会话管理和异常检测都做完了。

几个覆盖 MFA 全链路的第三方服务:

Auth0(Okta 旗下)覆盖范围最广,从短信到 TOTP 到 Passkey 都支持,企业 SSO 和合规审计功能也最全。定价偏高,适合已经有企业客户或有强合规需求的场景。

Clerk 和 Stytch 偏重开发者体验,文档和 SDK 质量较高。两个都覆盖了密码、社交登录、短信、TOTP 和 Passkey,对 Conditional UI 的支持也比较早。Clerk 在 React 和 Next.js 生态里集成尤其深入。

WorkOS 聚焦 B2B SaaS,企业 SSO 加 MFA 的能力比较完整,对企业客户的 onboarding 流程有针对性优化,定价也偏高。

Hanko 和 Corbado 是 passkey-first 的服务,整个认证流围绕 passkey 设计。纯 passkey 路线体验最干净,但需要用户群设备较新。

Supabase Auth 和 Firebase Auth 提供了开箱即用的 MFA 支持,包括 TOTP 和多设备 passkey,适合已经在用这两个平台做后端的团队。接入成本低,自定义空间相对有限。

如果选择自己拼,Twilio 和 Infobip 是短信发送的常用选择,TOTP 可以用开源库在服务端自己算,Passkey 通过 SimpleWebAuthn 这类库对接 WebAuthn 标准。自己拼的灵活性最高,但需要自己维护不同 MFA 方式之间的回退链路和短信发送的费率管理。

鸭哥每日手记

日更的深度AI新闻和分析