题目与适用场景
为一个消费级 Web 应用设计自助邮件链接密码重置流程。请求 API 接收邮箱;若账户存在,系统 发送一条包含不透明令牌的 HTTPS 链接。令牌从创建起 30 分钟后过期。数据库不得保存原始承载 令牌。
设计必须防止未登录调用者判断某个邮箱是否注册、向一个邮箱反复轰炸重置邮件、重放已使用的 链接、修改其他用户的密码,或在两次并发提交中绕过一次性约束。重置成功后,系统要更新密码、 让该账户所有尚未使用的重置令牌失效、撤销现有登录会话,并发送安全通知。密码重置不能移除或 替换已绑定的 MFA 验证器。
这道题主要考察后端安全与一致性。完整回答要把威胁模型落实到 API 契约、令牌生命周期、数据库 事务、邮件投递、会话模型和运行证据。随机令牌加过期时间只是起点;真正困难的是让所有可观测 路径和并发路径都遵守同一份安全契约。
面试官在考察什么
第一个信号是威胁建模。候选人应识别账户枚举、重置邮件轰炸、Host 头注入、URL 泄露、令牌 猜测、数据库泄露、链接重放、客户端篡改、并发消费、被盗会话和 MFA 绕过。每项威胁都要对应 具体控制,不能只笼统声称接口是安全的。
第二个信号是能否区分原始令牌和数据库中的校验值。邮件携带的是高熵承载密钥,数据库只存 单向摘要,因此读取令牌表不会直接得到可用链接。候选人还应区分随机令牌和人类密码:用户选择 的密码需要慢速密码哈希;均匀随机的 256 位令牌可以用快速密码学摘要建立索引,不会因此变得 可猜。
第三个信号是事务设计。应用代码先检查 used_at、之后再更新,会产生重放竞态。若同一用户 有两个不同的有效令牌,还需要把账户设为串行点。高质量方案会锁定用户行,在一个短事务内有 条件地消费当前令牌、修改密码、撤销同账户其他令牌和会话,并写入 outbox 事件。
第四个信号是恢复边界判断。忘记密码不代表可以停用 MFA 或替换丢失的第二因子。更高保证级别 的账户恢复需要单独的身份核验流程。回答还应拒绝把安全问题当作唯一恢复方式,也不能通过邮件 发送旧密码或系统生成的新密码。
最后还要看运行层面的思考。如果时间差、限流行为、日志、分析系统或客服工具仍会泄露账户是否 存在,统一文案就没有真正解决枚举。邮件投递故障不能提前改变账户状态,监控也必须在不记录 承载令牌的前提下发现滥用。
回答前要先确认的问题
- 这是替换密码还是完整账户恢复? 用户仍能控制已验证邮箱、只是忘记密码时,链接可以
替换密码。若邮箱、MFA 和恢复码都丢失,就需要更强的独立恢复流程。
- 账户是否启用 MFA? 密码重置应保留已绑定因子。如果产品想用一条流程同时恢复二者,
身份保证和滥用审查要求会显著变化。
- 哪些会话需要撤销? 本题要求撤销全部会话。若希望发起重置的设备保持登录,必须证明该
设备原本可信,并把例外写进契约。
- 能否同时存在多个链接? 保留少量且有上限的链接,可以避免后一封邮件先到时让合法链接
失效。无论哪个链接成功,都必须撤销该用户的其他所有链接。
- 账户风险和投递渠道是什么? 邮件可能适合低风险消费账户,却未必足以保护金融或受监管
访问。短信、恢复码、客服核验和等待期有不同的接管与可用性风险。
- 登录已经使用什么密码策略? 重置必须复用相同的长度、弱密码黑名单和哈希策略。单独设
一套更弱的重置规则会形成认证绕过。
- 滥用预算和邮件供应商限额是多少? 限流至少要考虑账户、IP、设备和供应商全局容量。只按
账户硬锁定,会让攻击者拒绝已知受害者的恢复请求。
30 秒回答框架
“我会对所有邮箱返回相同的 202 状态和文案,再在分层限流后异步完成账户查询与投递。账户存在 时生成 32 个随机字节,把 base64url 令牌放入由固定 HTTPS 域名构造的邮件链接,数据库只存令牌 摘要、用户和 30 分钟过期时间。GET 页面不消费令牌。最终 POST 会重新校验令牌并计算新密码 哈希,然后锁定用户行,在一个事务内标记令牌已用、更新密码、撤销其他重置令牌和所有会话, 同时写入通知与审计 outbox。并发或重放请求会在条件更新处失败。MFA 恢复继续走独立流程。”
分步骤深入解答
先定义两个公开端点,并刻意保持响应契约简单:
POST /password-reset-requests
{ "email": "person@example.com" }
202 Accepted
{ "message": "If an account matches, reset instructions will be sent." }
POST /password-resets
{ "token": "opaque-base64url-value", "new_password": "..." }无论账户是否存在,请求端点都返回相同状态、文案结构和缓存策略。它应先把工作交给有容量上限 的队列再返回,避免数据库快速退出或同步 SMTP 调用形成明显的时间旁路。固定休眠并不能解决 问题:队列饱和、只按账户限流和不同异常路径仍可能暴露行为,或者被利用来拒绝服务。
邮箱只能按照产品已经核验的身份规则规范化。域名转小写可能合理,但不能对所有域名套用删除 点号或加号标签等特定供应商规则,否则可能把两个独立账户合并。限流应同时覆盖规范化账户键、 来源 IP 或网络、设备或风险信号,以及邮件供应商的全局预算。可疑流量可以升级为挑战。公开响应 始终保持统一,而且仅仅发起“忘记密码”请求绝不能锁定账户或修改密码。
对真实账户,使用密码学安全随机数生成器产生 32 个字节,即 256 位;不带填充的 base64url 编码后是 43 个字符。这是本题的设计选择,不是通用过期时间或编码规定。数据库保存 SHA-256(raw_token),原始值只进入邮件链接。由于输入均匀随机且熵很高,快速单向摘要既能 建立索引,也不会使令牌可猜;把摘要本身当作承载令牌提交时,服务端会再次哈希,所以无法匹配。 人类选择的密码熵较低,必须另用慢速、带盐的密码哈希。
下面是一份示意性的 PostgreSQL 表结构:
CREATE TABLE password_reset_tokens (
id uuid PRIMARY KEY,
user_id uuid NOT NULL REFERENCES users(id),
token_digest bytea NOT NULL UNIQUE,
created_at timestamptz NOT NULL,
expires_at timestamptz NOT NULL,
used_at timestamptz,
revoked_at timestamptz
);
CREATE INDEX password_reset_tokens_active_user_idx
ON password_reset_tokens (user_id, expires_at)
WHERE used_at IS NULL AND revoked_at IS NULL;邮件 URL 必须使用已配置或白名单内的源站,不能信任请求中的 Host 头。链接使用 HTTPS;应用、 代理、分析和错误日志都要脱敏令牌;重置页不要加载第三方资源。设置 no-referrer 策略,防止跳转 泄露查询参数中的令牌。GET 可以展示表单或提示链接无效,但不能消费令牌,因为邮件安全扫描器 和链接预览经常会在用户之前访问链接。
不能信任 GET 阶段做过的校验。篡改后的客户端可以直接调用最终接口,因此真正修改密码的 POST 必须再次接收并校验令牌。开启事务前,先计算令牌摘要、查找未过期候选记录、校验新密码并计算 慢速密码哈希。这样,昂贵的哈希和绝大多数无效请求都在账户锁之外完成。该端点同样需要限流。
最终状态变更以用户行为串行点:
candidate = find token by SHA-256(raw_token)
reject publicly if candidate is missing, expired, used, or revoked
new_hash = Argon2id(new_password, fresh_salt, tuned_parameters)
BEGIN
SELECT id FROM users WHERE id = candidate.user_id FOR UPDATE
UPDATE password_reset_tokens
SET used_at = now()
WHERE id = candidate.id
AND used_at IS NULL
AND revoked_at IS NULL
AND expires_at > now()
RETURNING user_id
if no row returned: ROLLBACK and reject
UPDATE users
SET password_hash = new_hash,
password_changed_at = now(),
auth_version = auth_version + 1
WHERE id = candidate.user_id
UPDATE password_reset_tokens
SET revoked_at = now()
WHERE user_id = candidate.user_id
AND id <> candidate.id
AND used_at IS NULL
AND revoked_at IS NULL
DELETE FROM sessions WHERE user_id = candidate.user_id
INSERT security_outbox(password_reset_succeeded, user_id, occurred_at)
COMMIT密码哈希在 BEGIN 前计算,但只有条件消费成功才提交。对新系统,OWASP 当前列出的 Argon2id 最低配置之一是 19 MiB 内存、2 次迭代和 1 个并行度;应在不压垮正常认证容量的前提下压测并 提高成本。哈希值需要携带算法和参数,方便以后升级。Bcrypt 是带输入长度限制的旧系统后备方案, 不能把它和 Argon2id 当成可以随意互换的同义词。
用户行锁解决两类竞态。两次请求提交同一个令牌时,只有第一条条件更新能写入 used_at。如果 它们提交同一用户的两个不同有效令牌,两者可能都通过最初读取并完成哈希,但只有一个先持有 用户锁。该事务会撤销另一令牌;第二个请求取得锁后,条件更新返回零行。最终密码只有一个胜出 值,所有失败请求都看到不可逆的终态。
数据库会话可以通过服务端删除立即失效;刷新令牌则撤销其服务端记录。对原本无状态的访问令牌, 只有每次访问受保护资源时都比较服务端 authversion 或 passwordchanged_at,版本递增才会 立即生效。若架构不做这一检查,撤销要等令牌自然过期。必须明确这一限制,不能把清除当前浏览器 Cookie 说成已撤销攻击者在其他设备上的会话。
成功通知和安全审计事件通过事务内 outbox 写入,提交后再投递。通知包含时间、账户恢复说明和 报告欺诈的入口,不包含旧密码或新密码。通知供应商故障应触发重试和告警,不能回滚已经修改的 密码。请求侧也要让令牌和邮件任务具备持久关联,避免数据库已提交、队列却失败后静默丢失请求; outbox 或单一事务任务存储可以封住这一边界。
允许少量且有上限的未使用链接,不要每次请求都立即撤销上一条。邮件可能延迟或乱序;如果立即 替换,攻击者反复发起请求就能让受害者打开的邮件持续过期。限制未完成记录数,达到上限时撤销 或过期最旧记录;成功后再撤销全部剩余记录。终态和过期记录按审计保留策略定期清理。
不透明服务端令牌通常比自包含 JWT 重置链接简单。签名 JWT 要实现立即一次性使用、用户级撤销 和密码修改竞态,仍然需要服务端状态;既然状态已经存在,随机令牌加摘要少了 claims 和解析路径。 短 PIN 适合手工输入,但熵远低于 256 位令牌,因此必须严格限制尝试次数,并在验证成功后只创建 权限受限的重置会话。
监控应覆盖各风险维度请求速率、队列延迟、供应商错误、投递结果、令牌校验失败、成功时令牌 年龄、重放尝试和会话撤销失败。原始邮箱、令牌、新密码和完整重置 URL 不能进入指标或日志。 告警既要识别全局攻击,也要识别单账户邮件轰炸,同时不能让公开响应变成账户查询接口。
测试要把流程当作状态机,而不是只验证 happy path。并行提交同一令牌,以及同一用户的两个 不同令牌,必须都只有一个成功。还要测试过期边界、成功后重用、同账户其他令牌撤销、不存在的 账户、队列和邮件故障、Host 头篡改、Referer 泄露、日志脱敏、限流维度、其他设备会话拒绝、通知 重试和启用 MFA 的账户。安全扫描器 GET 链接时不应消费令牌。
高质量示范回答
“我会把重置令牌看作临时验证器,并围绕完整生命周期设计。请求接口总是返回相同的 202 响应, 在账户、网络、设备和供应商维度做滥用控制后,异步完成查询与投递。仅仅输入一个邮箱,不会锁定 或改变账户。
账户存在时,我生成 32 个随机字节,把 base64url 值放进 HTTPS 链接,只保存 SHA-256 摘要、 用户 ID 和 30 分钟过期时间。重置域名来自配置而非 Host,页面不加载第三方资源,使用 no-referrer 策略,所有日志管道都脱敏令牌。GET 只展示表单,因为邮件扫描器可能提前打开链接。
最终 POST 会再次校验令牌,在短事务前计算新密码哈希。事务内先锁用户行,再有条件地标记令牌 已使用,更新密码和认证版本,撤销同账户其他重置令牌与服务端会话,并写入 outbox。同令牌重放 会败在条件更新;用户行锁加同账户令牌撤销,让两个不同令牌也只能有一个胜出。
提交后,工作进程独立发送安全通知并重试。我会测试统一响应与时间差、令牌和 URL 泄露、并发 消费、过期、供应商故障、远端会话拒绝和 MFA 账户。密码重置保留 MFA;丢失所有验证器时改走 更高保证级别的独立账户恢复流程。”
常见错误
- 返回“邮箱不存在” → 攻击者可以枚举已注册账户 → **统一公开状态和文案,并避免快速退出
形成时间旁路。**
- 保存原始令牌 → 数据库读取者直接获得有效重置链接 → **只存单向摘要,除投递和用户提交
外全部脱敏原始值。**
- 使用最终表单中的用户 ID → 客户端可以替换目标账户 → 用户只能从服务端令牌记录推导。
- 在 GET 时消费链接 → 邮件扫描器可能在用户到达前使链接失效 → **只在修改密码的 POST 中
消费。**
- 只在 GET 校验、不在 POST 校验 → 伪造客户端可以绕过显示的表单 → **在最终服务端事务中
重新校验过期和终态。**
- 先检查、后无条件更新 → 并发请求可能同时通过检查 → **在事务内做条件更新,并以用户行串行
不同令牌。**
- 每次请求都让旧链接失效 → 攻击者可持续让受害者的最新邮件过期 → **保留有上限的集合,
只在一条成功后撤销其他链接。**
- 用
Host构造链接 → 被污染的请求会发送攻击者控制的重置域名 → **使用配置或白名单内的
HTTPS 源站。**
- 把令牌写进日志或分析系统 → 可观测平台变成凭证库 → **脱敏查询参数,并禁止重置页加载
第三方资源。**
- 重置后自动登录却没有会话策略 → 会话固定和被盗会话行为不明确 → **要求正常登录,并明确
撤销服务端会话。**
- 连同密码一起重置 MFA → 控制一个邮箱就能击穿更强因子 → MFA 恢复使用独立的风险流程。
- 通过邮件发送新密码 → 密码长期留在不安全渠道,攻击者还可锁死受害者 → **发送短期链接,
在有效证明提交前不改变账户。**
追问与回答
追问 1:如果产品使用无状态 JWT 访问令牌,怎么撤销会话?
服务端先撤销刷新令牌。若要立即让访问令牌失效,可在令牌中携带认证版本或签发时间,并让每次 访问受保护资源都与服务端当前状态比较。如果架构拒绝这次查询或撤销缓存,旧访问令牌会一直 有效到过期;应明确最长暴露时间,不能承诺立即退出。
追问 2:新请求是否应让所有旧重置链接立即失效?
通常不应在成功前全部撤销。邮件可能延迟或乱序,知道邮箱的攻击者也可能不断让受害者的最新 链接失效。保留少量且有上限的未过期令牌,控制请求量,并在成功修改密码的事务中撤销所有同 账户令牌。高风险产品可以选择严格替换,但必须接受相应的可用性代价。
追问 3:如果使用六位数字验证码而不是 URL 令牌,怎么改?
六位码的熵远低于 256 位令牌。要把它绑定到具体账户和权限受限的重置尝试,服务端限制尝试 次数并设置过期时间;验证成功后只创建短期重置会话。该会话不能变成普通登录会话,也不能允许 客户端选择用户 ID。
追问 4:新密码应采用什么规则?
复用正常登录的验证器策略。如果产品采纳当前 NIST 指引,单因子密码最短 15 个字符;只在 MFA 中使用的密码最短可为 8 个字符。至少允许 64 个字符,用黑名单拒绝常见或已泄露值,不增加任意 字符组合或定期轮换规则。密码哈希成本和输入规则分别调优。
追问 5:如果用户同时失去邮箱和 MFA,怎么办?
这属于完整账户恢复,不是本密码重置接口。按照账户风险使用预先绑定的恢复码或联系人、仍可用 的验证器、重新身份核验、等待期和人工审查。恢复地址本身的变更需要验证和独立通知。不能退回 到容易从公开信息猜出的安全问题,并把它当作唯一证明。
追问 6:如何证明账户枚举真的得到控制?
对存在和不存在的账户比较状态码、正文、响应头、缓存行为、延迟分布、限流切换和下游副作用。 测试要覆盖队列饱和和供应商故障,不能只看安静的本地环境。检查 CDN、代理、应用、分析和客服 日志是否出现标识符与完整 URL,并确认滥用看板只暴露聚合信号,不会变成账户存在性查询工具。