登录流程图指南
怎么画一张评审时真答得上问题的登录流程图:会话到底是什么、为什么所有失败长得一样,以及三个可直接改的例子。
一、什么是登录流程图
登录流程图 画的是一个匿名请求怎么变成一个有身份的请求。不是「密码怎么校验」—— 那一步是一条箭头加一次库调用。这张图真正的主题是校验通过之后交出去的那个东西:它是什么、活多久、存在哪、什么能让它失效。
网上几乎所有登录流程图都是同一张:一个表单方框、一个写着 密码对吗 的菱形、一条绿箭头去「首页」、一条红箭头回「登录页」。这张图不算错,只是它回答的问题没人有。没有人搞不清「密码错了该不该放进去」。人们真正搞不清的、以及线上真正会坏的,全在那个菱形省略掉的地方:「是」这条分支上到底创建了什么、「否」这条分支能走几次才会触发什么、以及这两条分支从外面看得出区别吗。
最后这一点,正是为什么它通常是 时序图而不是流程图。流程图画的是你服务端的推理过程;时序图画的是网线上真正传过去的东西 —— 而后者是攻击者唯一能看到的,也因此是唯一值得评审的。一旦你把「响应」画成箭头而不是把「结果」画成方框,「用户不存在」和「密码错误」就不再是你代码里的两条分支,而变成了一个陌生人能分辨的两个字符串。
这一页只画密码这条路。第二因子和「用 Google 登录」那个按钮各自只在一个点上接进来,而且各有各的页面 —— 折进这里只会得到一张有三个开头的图。
二、一张登录流程图要画出哪些东西
六样。如果你的图里有那个菱形却没有下面这些,它画的是一个表单,不是一次登录。
- 会话,一个有名字有寿命的具体东西。 不要写「让用户登录」—— 画出创建了什么、存在哪、活多久:
sidcookie、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例 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只读那些指回用户的箭头
把画完的图上除了「指回用户」的箭头之外全部遮住。那些是攻击者唯一能观察到的东西。然后把它们当成一份清单来读,不管每条来自哪个分支。
如果其中任意两条有区别 —— 状态码不同、措辞不同、耗时明显不同 —— 而这个区别用户本不该知道,那你就找到了一处信息泄漏。这是一个两分钟的动作,不需要任何工具,而它抓的正是登录系统里最常见的那个真问题 —— 不是哈希算法弱,也不是没上 HTTPS,而是一个乐意告诉陌生人「你的哪些用户存在」的登录页。
常见问题
登录流程该画流程图还是时序图?
时序图,几乎所有情况下都是。登录里的判断是琐碎的 —— 这个哈希等于那个哈希吗 —— 而有意思的内容是谁跟谁说话、回了什么,这正是时序图的用途。只有当读者是非技术人员、重点是用户旅程而不是系统行为时才用流程图,并且要接受这种图没法做安全评审,因为它根本不画响应。流程图真正胜出的场景只有一个:给设计交接用的、一屏一屏的多步登录地图。
两步验证和第三方登录画在图的哪里?
两者各自只接在一个点上,而且各自值得单独一张图。两步验证 接在密码校验和创建会话之间 —— 服务端返回的不是会话,而是一个短期的 pre-auth token,真正的会话要等验证码通过才签发。第三方登录 则整个取代密码校验:你的服务从不接触凭据,而是从提供方那里收到一份身份断言,再据此创建会话。在登录图上把这两个位置各标一条箭头就够了。合成一张的图会有三个入口,读者稳定地把它误读成「一条流程,中间可选」。
用 JWT 而不是服务端会话,图会变吗?
第一个例子几乎不变 —— 一条箭头从「创建会话记录」改成「签一个 token」。第二个例子变化很大,而这恰恰是两种方案之间那场诚实的争论。无状态 token 撤销不了,所以指向
Revoked的那些箭头要么消失,要么变成一张黑名单 —— 而黑名单就是换了张皮的会话存储。画出来,取舍就不再抽象了:要么「登出」在 token 过期之前都不是真的,要么你把想躲开的状态又请了回来。常见的折中 —— 短期 access token 加可撤销的 refresh token —— 值得单独画,因为它真正的复杂度全在换发那一步。图上该不该把「用户不存在」画成单独一条分支?
对内该画,对外不该 —— 而把两者同时画出来正是重点。你的服务端确实走了不同的代码路径,把它藏起来会让这张图变成谎言。必须一致的是离开你服务器的那条箭头:同样的状态码、同样的响应体、大致同样的耗时。所以先画内部分支,再把它的两端画成汇入同一条响应箭头。画出「分支汇合」的图,记录的是一个刻意的决策;完全没有分支的图,记录的是一份没人核对过的实现。
图上会话该写多久?
你实际用的是多少就写多少 —— 「写下来」的价值远高于「选对数字」的价值。给个起点:面向消费者的产品用 空闲 30 分钟 + 绝对 30 天;凡是牵涉钱或管理员权限的要短得多,空闲窗口小于 15 分钟、并在敏感操作前要求重新认证是常态。真正值得遵守的规则是:这个数字该写在箭头上,而不是待在没人看的配置文件里。活得比用途还久的会话是最安静的安全问题 —— 因为它从来不会出错。
免费吗?
免费。没登录每天 20 次,登录后每天 500 次。把本页任何一个例子在编辑器里打开完全不计次 —— 那条路径根本不打模型。