登录流程图指南

怎么画一张评审时真答得上问题的登录流程图:会话到底是什么、为什么所有失败长得一样,以及三个可直接改的例子。

发布于 ·9 分钟阅读
loginauthenticationsequence-diagramtemplate

一、什么是登录流程图

登录流程图 画的是一个匿名请求怎么变成一个有身份的请求。不是「密码怎么校验」—— 那一步是一条箭头加一次库调用。这张图真正的主题是校验通过之后交出去的那个东西:它是什么、活多久、存在哪、什么能让它失效。

网上几乎所有登录流程图都是同一张:一个表单方框、一个写着 密码对吗 的菱形、一条绿箭头去「首页」、一条红箭头回「登录页」。这张图不算错,只是它回答的问题没人有。没有人搞不清「密码错了该不该放进去」。人们真正搞不清的、以及线上真正会坏的,全在那个菱形省略掉的地方:「是」这条分支上到底创建了什么、「否」这条分支能走几次才会触发什么、以及这两条分支从外面看得出区别吗。

最后这一点,正是为什么它通常是 时序图而不是流程图。流程图画的是你服务端的推理过程;时序图画的是网线上真正传过去的东西 —— 而后者是攻击者唯一能看到的,也因此是唯一值得评审的。一旦你把「响应」画成箭头而不是把「结果」画成方框,「用户不存在」和「密码错误」就不再是你代码里的两条分支,而变成了一个陌生人能分辨的两个字符串。

这一页只画密码这条路。第二因子和「用 Google 登录」那个按钮各自只在一个点上接进来,而且各有各的页面 —— 折进这里只会得到一张有三个开头的图。

二、一张登录流程图要画出哪些东西

六样。如果你的图里有那个菱形却没有下面这些,它画的是一个表单,不是一次登录。

  • 会话,一个有名字有寿命的具体东西。 不要写「让用户登录」—— 画出创建了什么、存在哪、活多久:sid cookie、HttpOnly Secure SameSite=Lax、绝对有效期 30 天。登录设计引发的争论里,有一半其实是在吵没人写下来的 cookie 属性。
  • 所有失败共用一个响应。 密码错、邮箱不存在、账号被禁用、邮箱未验证 —— 回给用户的那条箭头应该是同一个状态码、同一句话。把它们画成汇入同一条箭头,因为一张画着四条措辞不同的报错箭头的图,本身就是那个「账号枚举」漏洞的图解。
  • 限流,以及它的两个主语。 「按账号」和「按 IP」是两种不同的控制,防的是两种攻击 —— 针对一个用户的撞库,和横扫很多用户的喷洒。两个都要带数字画在图上,锁定后的响应也要画,因为「429 带 retry-after」和「装作密码错了」是两个不同的产品。
  • 恒定耗时那一步。 即使数据库里没有这个用户,哈希校验也照跑。如果你的图在哈希那一步之前就分支去「返回 401」,它记录的是一个计时探测器:邮箱不存在的路径 2 毫秒返回,密码错误的路径 200 毫秒返回 —— 这个差值是机器可读的。
  • 两个接入点,各画一条箭头。 第二因子接在密码校验之后、签发会话之前;外部身份提供方则整个取代密码校验。把这两个位置标出来,这张图对自己的边界就是诚实的,读者也知道另一张图从哪儿开始。
  • 找回密码。 它不是一个客服功能,它是获得会话的第二条路,攻击者也是这么看它的。一张停在登录表单上的登录图,漏掉了自己一半的攻击面。

这是骨架 —— 所有人都会画的那一版,上面六样一样没有。它值得看一眼,恰恰因为它看起来是完整的。里面每条箭头都对;但这张图拿去评审毫无用处,因为上面没有一处能以有意思的方式出错。

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

    User->>App: email and password
    App->>Auth: sign in
    Auth->>DB: look up the user by email
    DB-->>Auth: the stored password hash
    Auth-->>App: session token
    App->>User: redirect to the dashboard
最小的登录流程图:用户提交邮箱密码,服务校验哈希并返回会话。

三、登录流程图怎么画

三步。第一步只是换个顺序,但价值基本都在那儿。

第 1 步 · 先画会话,不要先画校验

从结尾开始。在画表单的第一条箭头之前,先写下:登录成功之后,存在了哪些之前不存在的东西?

- 某处的一行记录,或者一个背后什么都没有的签名 token - 一个交到浏览器手上的标识,带着一组具体的 cookie 属性 - 一个过期时间 —— 而且很可能是两个:空闲过期和绝对过期 - 一份「什么能让它提前作废」的清单

然后倒着走。密码校验存在的意义,是批准创建上面那个对象;一旦顺序这么摆,图自己就写出来了:校验是一道闸门,所有有意思的东西都在闸门另一侧。

按常规顺序画 —— 表单、菱形、结束 —— 得到的图里,会话是被一条写着「成功」的箭头暗示出来的。寿命相关的 bug 就藏在这个词里,因为整张图从没逼任何人说出「成功」到底能持续多久。

第 2 步 · 让所有失败长得一模一样

把所有出错方式列出来,然后画成它们全部汇入同一条箭头:

- 没有这个邮箱的账号 - 账号存在,密码错 - 账号存在、密码对,但被禁用或邮箱未验证 - 尝试次数太多

只有最后一条允许长得不一样,而且是不得不 —— 你没法在限流某个人的同时不告诉他。其余全部用同一个状态码、同一句文案,最好连耗时都一样。

这一步最容易被跳过,而跳过它不是什么细微错误。一个注册页说 「这个邮箱已注册」,配上一个登录页说 「没有这个邮箱的账号」,合起来就是一个免费的会员查询接口:喂给它一份泄漏的邮箱列表,它会告诉你谁在这儿有账号。两句文案单独看都是好心,合起来是信息泄漏 —— 而这正是图能抓住、code review 抓不住的那类问题,因为这两个字符串在两个文件里,相隔几个月写成。

如果你的产品确实需要告诉用户账号被禁用了,那就放在密码校验通过 之后 说 —— 那时候你面对的是账号本人,不是陌生人。在图上,这只是把一条箭头挪到某个分支下面,而这就是全部的修复。

第 3 步 · 画出来

用句子描述,让 text2diagram 排版 ——「用户提交邮箱和密码,服务按邮箱和 IP 限流,查出用户,即使查不到也照跑哈希校验,成功则创建 30 天会话并下发 HttpOnly cookie;所有失败返回同一个 401」,还回来的是一段 alt 块已经正确嵌好、可以直接改的 sequenceDiagram

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

四、三个登录流程图例子

三张图,对应登录设计真正必须回答的三个问题:门口发生了什么、拿到手的那个东西随时间还值多少、以及没有它的人怎么回来。

例 1 · 密码登录,含那些通常被省掉的部分

这里有四个细节,是「照抄这张」而不是「凭记忆自己画」的理由。

限流在数据库查询 之前,而且写明了两个主语。放在之后,攻击者每次尝试仍然白拿一次查询 —— 而那是贵的那部分。

run it even when there is no row 看起来像给实现者的备注,它确实是,但它同时也是「登录接口」和「用户存在性查询接口」的分界。省掉它,耗时会把一切都讲出去。

签发会话那条箭头带着属性和有效期。这一个标签平息过的设计争论,比旁边任何一段文字都多。

失败分支是汇合的:查不到用户和密码错误产生同一条箭头,这是故意画成一条的。如果你在自己的图上能找出两条可分辨的失败箭头,那你什么都没跑就已经找到一个 bug 了。

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

    User->>App: email and password
    App->>Auth: sign in
    Auth->>Auth: rate limit check - 5 per email, 20 per ip, 15 min
    alt over the limit
        Auth-->>App: 429 with retry after
        App->>User: too many attempts, try again later
    else within the limit
        Auth->>DB: load the user by email
        DB-->>Auth: a row, or nothing
        Auth->>Auth: verify the hash - run it even when there is no row
        alt hash matches and the account is active
            Auth->>DB: create session, idle 30 min, absolute 30 days
            Auth-->>App: Set-Cookie sid, HttpOnly Secure SameSite=Lax
            App->>User: redirect to the dashboard
        else no row, wrong password, or account disabled
            Auth->>Auth: count this attempt against both limits
            Auth-->>App: 401 email or password is incorrect
            App->>User: one message for every failure above
        end
    end
密码登录时序图,包含按账号和按 IP 的限流、恒定耗时的哈希校验、带属性的会话 cookie,以及共用的单一失败响应。

例 2 · 登录之后,那个会话在干什么

这张用状态图,因为这一段不是对话 —— 是一个对象在变老。把它单独画出来,「会话」才不会只是某条箭头上的一个词。

两个过期时间是重点。空闲过期 是滑动窗口,每次请求都重置;绝对过期 永不重置。只有滑动窗口的系统,会签发出「只要某处还开着一个标签页就永远有效」的会话;只有绝对过期的系统,会在某个随机时刻把人从任务中间踢出去。几乎所有人两个都想要,而几乎没有人把两个都写下来。

Revoked 是评审时真正值得吵的那条转移。当用户因为怀疑密码泄漏而改密码时,唯一符合他意图的响应是把所有地方的所有会话全部杀掉 —— 包括攻击者那个。如果你的图上没有一条从 改密码Revoked 的箭头,那改密码对已经在里面的人毫无影响。

看 Mermaid 源码
stateDiagram-v2
    [*] --> Anonymous
    Anonymous --> Active: password verified, session created
    Active --> Active: request inside the idle window
    Active --> Idle: 30 minutes with no request
    Idle --> Active: a request arrives, window slides
    Idle --> Expired: idle window ran out
    Active --> Expired: 30 days since it was created
    Active --> Revoked: signed out, password changed, or admin action
    Expired --> Anonymous
    Revoked --> Anonymous
    note right of Revoked
        a password change revokes every session
        including the ones on devices we cannot see
    end note
登录会话的状态图:活跃、滑动窗口下的空闲、被空闲或绝对超时判过期,以及被登出或改密码撤销。

例 3 · 找回密码 —— 拿到会话的另一条路

把这张图和登录图放在一起画,不要另开文档 —— 因为它授予的东西和登录一模一样,而保护通常松得多。

第一条响应本身就是整个设计:不管账号存不存在都返回 200,而且是在查任何东西之前就返回。别的顺序都会泄漏。给用户看的文案也得配套 —— 「如果这个邮箱已注册,我们已发送链接」—— 是的,这句文案略差。但它也是唯一一句不会把这个表单变成「你的登录页拒绝成为的那个枚举接口」的写法。

token 一次性、短有效期,两条都重要,因为重置链接落在邮箱里 —— 而邮箱会被转发、被备份、被同步到没人记得还在用的设备上。

撤销那条箭头是最常被实现漏掉的。会来重置密码的人,往往正是已经被入侵的人。如果旧会话能挺过这次重置,那这次重置什么也没做成。

看 Mermaid 源码
sequenceDiagram
    autonumber
    participant User
    participant App as Web App
    participant Auth as Auth Service
    participant Mail as Email Service

    User->>App: I forgot my password, here is my email
    App->>Auth: start a reset
    Auth-->>App: 200 - the same answer whether or not it exists
    App->>User: if that email is registered we sent a link
    opt the account actually exists
        Auth->>Auth: mint a single use token, valid 30 minutes
        Auth->>Mail: send the reset link
        Mail->>User: reset link
        User->>App: open the link and choose a new password
        App->>Auth: redeem the token
        alt token unused and still inside the window
            Auth->>Auth: store the new hash, burn the token
            Auth->>Auth: revoke every existing session
            Auth-->>App: done, sign in again
        else token used, expired, or unknown
            Auth-->>App: 400 ask for a fresh link
        end
    end
找回密码时序图:不泄漏账号是否存在的响应、一次性且限时的 token,以及重置后撤销全部现有会话。

只读那些指回用户的箭头

把画完的图上除了「指回用户」的箭头之外全部遮住。那些是攻击者唯一能观察到的东西。然后把它们当成一份清单来读,不管每条来自哪个分支。

如果其中任意两条有区别 —— 状态码不同、措辞不同、耗时明显不同 —— 而这个区别用户本不该知道,那你就找到了一处信息泄漏。这是一个两分钟的动作,不需要任何工具,而它抓的正是登录系统里最常见的那个真问题 —— 不是哈希算法弱,也不是没上 HTTPS,而是一个乐意告诉陌生人「你的哪些用户存在」的登录页。

常见问题

接着读

去试试 text2diagram

打开工具
← 回到全部教程