两步验证流程图指南

怎么画一张真正够用的两步验证流程图:半登录状态、锁定分支,以及三个可直接改的例子 —— TOTP、短信验证码、开启流程。

发布于 ·9 分钟阅读
2faauthenticationsequence-diagramtemplate

一、什么是两步验证流程图

两步验证流程图 画的是「密码正确」到「用户登录成功」之间发生了什么 —— 也就是第二因子那一段。它是一张时序图,因为这一步牵涉到几个很容易被混为一谈的角色:你的应用、校验验证码的那个东西,以及 产生 验证码的那个东西 —— 最后这个通常根本不在你的控制之下。

让这张图值得画的核心事实是:两步验证把登录拆成了两笔事务,并且在中间造出一个状态。密码校验通过之后,用户既不是未登录也不是已登录,而是别的什么 —— 半登录 —— 这个状态需要自己的短期凭据、自己的过期时间、自己的失败计数。跳过它的图会从「密码正确」直接画一条箭头到「请输入验证码」,暗示服务器正把用户身份存在某个变量里。绝大多数真实的两步验证漏洞就住在这个暗示里。

第二个理由是这个流程主要由失败分支组成。成功路径就四条箭头,没人有异议。团队会吵的、以及工单的来源,是这些:错几次锁账号?短信一直不到怎么办?手机掉河里了用户怎么办?旧验证码能不能重放?每一条都是一个分支,而你没画的分支,总有人会在某次热修里临时发明一个。

这一页只画第二因子。骨架图里出现密码,只是为了交代交接点在哪。

二、一张两步验证流程图要画出哪些东西

六样。第一样就是「真图」和「宣传插画」的分水岭:

  • 半登录状态,而且要有名字。 把密码校验通过后服务端发回的那个凭据画出来 —— pre_auth_tokenmfa_challenge,叫什么都行 —— 并把有效期写在箭头上。如果你的图里没有这个凭据,那它描述的是一个「第二因子之前会话就已经存在」的系统。
  • 谁生成验证码、谁校验验证码。 TOTP 里这是两个从不通信的角色 —— 验证器 App 和你的服务器各自用同一个共享密钥算出同一个数字。把 App 画成一条生命线,这件事一目了然;写成文字则永远说不清。
  • 失败计数,以及用完之后会怎样。 「验证码错了」不是一条分支,是两条:错了还能再试,和错了到此为止。把数字写到图上。
  • 时间窗。 TOTP 的验证码按 30 秒一个步长,通常前后各容忍一步;短信验证码有一个你自己定的有效期。两者都该写在图上 —— 因为「明明看着对的验证码为什么被拒」是一个答案在时钟偏差上的客服问题。
  • 一次性。 校验通过的验证码必须被标记为已消费,否则一个被人从背后瞄到的验证码在这个时间窗剩下的时间里一直有效。这是一条自箭头,是这份清单上最便宜的一项。
  • 一条回得来的路。 没有恢复路径的两步验证图,画的是一个会把自己用户锁在门外的系统。恢复码、备用因子,或者一套客服流程 —— 你实际有哪个就画哪个。

骨架,一条失败分支都没有。注意它有多短 —— 这就是全部的成功路径,也正是为什么一张停在这里的图几乎什么都没说。

看 Mermaid 源码
sequenceDiagram
    participant User
    participant App as Web App
    participant Auth as Auth Service
    participant DB as User Store

    User->>App: username and password
    App->>Auth: verify credentials
    Auth->>DB: look up the password hash
    DB-->>Auth: hash and two_factor_enabled
    Auth-->>App: password ok, second factor required
    App->>User: ask for the 6 digit code
    User->>App: 123456
    App->>Auth: verify the code
    Auth-->>App: code ok, session issued
    App->>User: signed in
最小的两步验证流程图:校验密码、要求第二因子、输入验证码、签发会话。

三、两步验证流程图怎么画

三步。第 2 步是这张图从「插图」变成「评审」的地方。

第 1 步 · 定下是哪种因子,以及它归谁

三种常见的第二因子会画出三张不同的图,而区别完全在于 哪些参与方不归你管:

TOTP(验证器 App)—— 验证码的生成方是用户手机上的一个 App,和你之间没有任何网络连接。它永远不会出现在指向你服务器的箭头上。你的图里会有一条只和用户说话的生命线。

短信 —— 中间多了一个第三方网关,于是多一个参与方、多一种失败模式(发不出去),还多一笔按次计的成本 —— 这让「重发限流」变成一个商业决策,而不只是安全决策。

Passkey / 硬件密钥 —— 由浏览器居中协调,有意思的箭头是挑战和签名,而不是一串人手工重敲的数字。差别大到值得单独一张图,不该当成这张图的一条分支。

一张图一种因子。如果你两种都支持,就画两张 —— 一旦把短信兜底折进 TOTP 那张图,两条路径都变难读,而兜底那条恰好是攻击者会瞄准的。

第 2 步 · 先画失败

反常识地,从末尾开始。在画第一条成功箭头之前,先把所有失败方式写下来:

- 验证码错了,还有重试次数 - 验证码错了,重试次数用完 - 验证码对,但晚了(超出时间窗) - 验证码对,但已经用过 - 密码对,用户名不存在 —— 响应有区别吗?一定不能有 - 验证码根本没收到(只有短信会) - 用户已经彻底没有这个因子了

然后再画成功路径,把每个失败挂到「它被检测出来的那条箭头」上。顺序很重要:先搭成功路径,失败就会被塞进任何有空的地方,得到一张技术上完整、但找不出缺口的图。先列失败,缺口就是那个你哪儿都挂不上去的分支。

第五条是最容易翻车的。如果用户名错和密码错产生可见不同的响应 —— 文案不同、状态码不同,或者耗时明显不同 —— 这个登录页就是一个用户名探测器。而两步验证这一步最容易泄漏,因为「这个账号需要验证码」这句话很容易在密码都还没校验的时候就说出口。

第 3 步 · 画出来

用句子描述,让 text2diagram 排版 ——「密码校验通过后服务端返回一个五分钟有效的 pre-auth token,用户从验证器 App 读一个验证码提交上来,服务端用共享密钥校验、标记已消费、签发会话;连错五次就把 pre-auth token 作废」,还回来的是一段带好 alt 块、可以直接改的 sequenceDiagram

或者把下面三张打开改参与方名字。值得留下的是分支结构。

四、三个两步验证流程图例子

三张图:最常见那种的完整版、中间夹着第三方的那种,以及必须发生在前两种之前的那一种。

例 1 · 登录时校验 TOTP,含锁定

这里有三个细节值得抄。带五分钟有效期的 pre_auth_token,就是上面清单里那个半登录状态的具体形态。mark this code consumed 是重放防护 —— 一条自箭头,很容易漏,而漏掉就意味着被人从背后瞄到的验证码在这个时间窗剩下的时间里一直能用。

第三个是第一条分支:密码错的时候,响应里不透露这个账号有没有开两步验证。哪怕服务端其实已经知道了,也必须如此 —— 这也是为什么这条分支在 pre-auth token 之前 而不是之后。先查两步验证状态的图,记录的是一个账号枚举接口。

还要注意验证器 App 从来没有一条指向服务器的箭头。两边各自用开启时约定好的密钥算出同样的六位数,此后再无往来。

看 Mermaid 源码
sequenceDiagram
    autonumber
    participant User
    participant App as Web App
    participant Auth as Auth Service
    participant TOTP as Authenticator App

    User->>App: username and password
    App->>Auth: verify credentials
    alt password wrong
        Auth-->>App: 401 - never reveal whether 2fa is on
        App->>User: generic sign in failed
    else password correct, 2fa enabled
        Auth-->>App: pre_auth_token, valid 5 minutes
        App->>User: enter the 6 digit code
        User->>TOTP: read the current code
        TOTP-->>User: 123456
        User->>App: 123456
        App->>Auth: verify code with pre_auth_token
        alt code matches inside the time window
            Auth->>Auth: mark this code consumed
            Auth-->>App: full session
            App->>User: signed in
        else wrong code, attempts left
            Auth-->>App: 401 with attempts remaining
            App->>User: try again
        else 5 failed attempts
            Auth->>Auth: burn the pre_auth_token
            Auth-->>App: 429 start over from the password
        end
    end
TOTP 两步验证登录流程图,包含 pre-auth token、验证码消费、重试计数,以及连错五次后的锁定。

例 2 · 短信验证码,含那条永远没送达的短信

多出来的这个参与方对图的改变,被低估得厉害。网关接受了你的请求(queued)和手机收到短信 不是 同一件事,所以这里是两条箭头,而第二条你观察不到。这个断口就是「重发」这个功能存在的原因,也是它必须限流的原因 —— 这里画成注释而不是分支,因为它管的是整场交互,不是某一步。

失败分支是评审时真正值得吵的:网关挂了的时候,用户是拿到一个备用方式,还是撞上一堵墙?每一个用短信做两步验证的系统都需要一个答案,而相当多的系统的答案是「一堵墙」—— 仅仅因为没人画过这条箭头。

看 Mermaid 源码
sequenceDiagram
    autonumber
    participant User
    participant App as Web App
    participant Auth as Auth Service
    participant SMS as SMS Gateway
    participant Phone as User Phone

    App->>Auth: password verified, send a code
    Auth->>Auth: generate 6 digits, store the hash, ttl 5 min
    Auth->>SMS: send to the stored number
    alt gateway accepted it
        SMS-->>Auth: queued
        SMS->>Phone: your code is 123456
        User->>App: 123456
        App->>Auth: verify the code
        Auth-->>App: full session
    else gateway rejected or timed out
        SMS--xAuth: delivery failed
        Auth-->>App: 502 could not send
        App->>User: offer a backup method
    end
    Note over App,Auth: resend is rate limited - 1 per minute, 3 per hour
短信两步验证流程图,包含短信网关参与方、发送失败分支和重发限流。

例 3 · 开启两步验证

所有人第二个实现、却从来不画的那条流程。里面有两处是承重的。

密钥在验证码证明它可用之前一直是待定状态。 二维码一显示就把两步验证开起来,会把每一个还没扫就关掉标签页的用户锁在门外 —— 一条自己造出来的工单队列。那条自箭头上的 status pending 和底下那条拒绝分支,是同一个决策的两面。

恢复码只能在这里发,别处都不行。 这是整个系统中唯一一个「用户已经通过认证、但还没有依赖新因子」的时刻,因此也是唯一能安全地把「回来的路」交给他的地方。注意 shown once:它们是像密码一样哈希存储的,所以这个页面是真的没法事后再渲染一次。

看 Mermaid 源码
sequenceDiagram
    autonumber
    participant User
    participant App as Web App
    participant Auth as Auth Service
    participant TOTP as Authenticator App

    User->>App: turn on two factor auth
    App->>Auth: start enrollment
    Auth->>Auth: generate a shared secret, status pending
    Auth-->>App: secret as a QR code
    App->>User: scan this code
    User->>TOTP: scan
    TOTP-->>User: 654321
    User->>App: 654321
    App->>Auth: confirm enrollment with that code
    alt code matches the pending secret
        Auth->>Auth: activate 2fa, mint 10 recovery codes
        Auth-->>App: recovery codes, shown once
        App->>User: save these somewhere offline
    else code does not match
        Auth-->>App: 400 - secret stays pending, 2fa still off
        App->>User: scan again
    end
两步验证开启流程图:待定的共享密钥、扫码、激活前先验证,以及一次性恢复码。

只读半登录那一段,问它能干什么

把画完的图拿来,把「密码正确」到「签发会话」之间以外的部分全遮住。在这一段里,用户手上握着一个「不是会话」的凭据。然后问三个问题:这个凭据能碰到校验接口以外的任何接口吗?它活得比它在等的那个验证码还久吗?第一个还活着的时候能再签发第二个吗?

答案应该是不能、不会、不能。凡是答不上「不」的地方,那就是一个真 bug —— 一个「只要调对接口,光有密码就够了」的账号 —— 而它在 code review 里是看不见的,因为 pre-auth token 和它能碰到的那些接口通常不在一个文件里。在图上,它只是一块被遮住的矩形。

常见问题

接着读

去试试 text2diagram

打开工具
← 回到全部教程