登录限流的失败计数:避免正确登录也被锁住

登录限流的失败计数:避免正确登录也被锁住原创封面

给不同计数器分别定义用途

登录接口需要限制自动化攻击,但如果每次请求都增加认证失败次数,用户连续几次正确登录也可能被锁住。问题在于把请求总量和凭据验证失败混成了同一个计数。请求总量保护计算资源与网络,失败次数保护认证入口,两者应有独立的名称、窗口和恢复规则。先定义事件含义,再选择存储与阈值,避免代码中出现一个被所有分支随意调用的通用计数函数。

本文采用简化的认证流程讨论状态变化,不给出适合全部站点的固定阈值。OWASP 认证与拒绝服务指南分别说明防暴力尝试和资源保护的考虑。实际方案还应结合账号风险、共享网络和多因素认证设计。限流正确不等于认证系统完整,密码存储、会话管理和错误信息也需要遵循各自边界。

检查额度与增加失败必须分开

请求开始时,可以检查当前失败额度是否已经达到限制,但这次检查不应自动计入一次新失败。只有明确完成了凭据验证并得到失败结果,才增加对应计数。否则缺少输入、重复加载界面或成功认证都可能消耗失败额度。函数名应表达动作区别,例如检查可用额度、记录失败和清除指定范围,便于审查每个分支的调用位置。

请求总量限制可以放在更早的入口,覆盖无效输入和高频重试,但应使用独立桶及适当响应。失败限制则依据经过核验的来源和账号上下文处理。两种限制同时存在时,返回信息要让客户端知道可以何时重试,不需要泄露具体账号是否存在。记录内部原因码即可,避免把整个认证判断过程返回给未认证请求。

账号失败与来源失败作用不同

按账号记录失败能够应对分散来源的攻击,但攻击者也可能借此反复锁住其他人的账号。按来源记录能够控制单个出口的攻击,却会影响共享网络中的正常用户。可以组合不同范围并采用渐进延迟、临时锁定及额外验证,而不是只依赖一个永久封禁字段。每种机制都需要有明确过期时间和恢复方式。

账号计数通常属于账号数据,来源计数可以采用短期存储并限制保留时间。对于不存在的账号,也要保持合适的响应一致性,同时防止任意账号字符串造成无限数据增长。计数键应经过规范化和长度限制,避免输入大小成为新的资源消耗入口。日志采用受控标识,不能记录明文密码或把认证请求体整段保留。

成功认证只清理它负责的范围

成功登录可以重置当前账号的连续失败状态,并按设计清理该请求对应的失败桶。不能清空整个站点的所有失败记录,也不应让一个账号的成功登录消除其他账号的锁定。请求总量保护还应继续遵循自己的时间窗口,否则攻击者利用一组正确凭据就可能不断重置资源限制。清理动作需要与成功建立会话的流程对应。

成功分支也要处理数据库写入和会话创建可能失败的情况。如果凭据验证通过但状态保存失败,应返回系统错误,不能声称登录完成,更不应增加凭据失败次数。记录审计时说明认证结果和会话结果的差别。避免在客户端收到成功之前执行会影响全部风险状态的宽泛清理,这会让半完成流程产生意外副作用。

多因素挑战不是普通密码失败

密码正确但还需要第二因素时,系统进入挑战阶段,并没有完成登录,也不一定发生认证失败。缺少验证码可以返回明确的挑战响应,而错误验证码则按对应规则计入失败。否则页面正常展示第二步时就消耗一次额度,用户几次切换界面后可能被锁住。挑战请求还应绑定账号与上下文,避免绕过第一步验证。

第二因素重放、过期和格式错误可以有不同内部原因,但对外提示要保持适当克制。来源失败与账号失败是否共同更新,由风险策略明确规定,并测试边界。完成第二因素后再建立正式会话,清除相应状态。不能因为密码正确就提前取消所有限制,因为挑战阶段本身也可能遭受自动化尝试。

数据库故障不应被计成凭据错误

查找账号、读取失败桶或保存状态时出现数据库故障,属于系统无法完成认证,不是用户输入了错误密码。捕获异常时必须区分预期认证失败与基础设施异常。把所有异常都放进错误密码分支,会在系统故障期间批量增加失败计数,恢复后正常用户仍然被锁住。错误状态也会污染审计,让运维误判为攻击突然增多。

对系统错误应返回可理解的暂时不可用提示,并在服务日志中保留受控原因。是否采用降级限制需要谨慎设计:多进程各自使用内存桶可能形成不同额度,重启也会清空状态。不能默默降级后仍宣称限制保持不变。关键入口可以选择在依赖故障时停止认证,恢复后继续原来的窗口和账号状态,避免凭据验证与风险判断被分离。

并发计数需要数据库原子操作

先读取计数,再在应用中加一,最后写回,可能在并发请求下丢失更新。应采用数据库原子更新、受控事务或存储提供的计数操作,并定义窗口到期后的重置方式。计数增加和过期时间处理应处于同一个原子过程,避免新窗口继承旧计数。检查失败额度与验证之间仍存在并发间隙,需要结合实际策略决定如何处理已在途请求。

如果窗口边界允许少量已经进入验证的请求继续执行,应明确这个行为,不能声称任何时刻都绝对只有阈值以内的尝试。资源总量限制可以帮助控制在途数量,账号失败更新则在验证完成后原子提交。验收时同时发起多个失败请求,比较实际计数和拒绝结果,而不是仅用串行循环测试后推断并发也正确。

重试信息要指导恢复行为

临时限制响应可以带上重试等待时间,界面显示清晰的恢复提示,并避免自动高频重试。等待时间依据当前窗口剩余时长计算,而不是每次响应都给一个重新开始的完整窗口。客户端倒计时只是展示,服务端仍然必须在每次请求时校验状态。刷新页面不能绕过限制,也不应该因刷新而再次增加凭据失败。

账号临时锁定与来源限制可能有不同恢复时刻,界面无需展示所有内部规则,但应保持提示一致且不误导。对于系统故障,告诉用户稍后重试;对于缺少第二因素,展示对应输入;对于普通失败,采用适当统一提示。不要将内部异常栈或数据库字段名直接显示为恢复办法,这些信息既不能帮助用户,也会暴露实现细节。

用状态变化表设计隔离测试

测试应覆盖正确密码、错误密码、不存在账号、缺少第二因素、错误第二因素、数据库故障和额度已用尽。为每种场景列出请求总量、账号失败、来源失败和会话状态的预期变化。先验证单次结果,再覆盖并发、窗口到期和应用重启。连续正确登录尤其值得保留为回归场景,它直接检验失败计数是否被错误放在请求入口。

隔离测试需要固定或可注入的当前时刻,避免等待真实窗口经过,也不能修改生产服务器时钟。使用专用账号和来源标识,测试结束清理本次状态。对于成功清理范围,提前建立另一个账号的失败记录,确认它没有变化。这样可以发现全局清空或作用域遗漏,而不只检查当前用户能够登录。

观察指标不能泄露认证信息

可以统计失败类别、限流命中、临时锁定和依赖错误,帮助区分真实攻击与程序错误。不要记录明文输入、完整验证码或会话凭据。来源标识按用途进行最小保存,账号标识也需要受控访问。发现限流突然集中到一个代理地址时,先检查客户端来源解析,再调整阈值,避免把信任链错误当成正常流量增长。

指标还应能区分请求被入口限制拒绝与凭据验证确实失败,两者在风险分析中含义不同。部署修复后观察一段受控流量,并保留规则版本,使变化可以追溯。不能只因单个用户成功登录就判断全站限制已经正确;还需验证其他用户的状态未被清除,以及失败记录在服务重启和多进程间仍然一致。

让认证分支直接表达计数语义

下方伪代码展示检查与增加失败的分离,凭据验证失败才调用失败记录,系统异常交给错误处理。它省略了真实密码验证、第二因素和会话细节,不能直接作为完整登录接口。实现时应把这些阶段补齐,并使状态保存、会话建立和审计具有一致的成功语义。每个计数函数都应限定账号或来源作用域。

代码审查时沿所有提前返回和异常分支检查计数,确认挑战、成功和系统故障没有误入失败分支。随后用真实存储执行状态变化测试,而不是只模拟计数函数调用次数。数据库原子行为与应用控制流缺一不可,最后再检查界面恢复提示和等待响应,形成从服务端状态到用户体验的完整验证路径。

实现片段

async function login(input: LoginInput, context: LoginContext) {
  await chargeRequestVolume(context) // 独立的资源保护范围
  await checkFailureAllowance(context) // 检查,不增加失败
  const account = await findAccount(input.account) // 系统异常继续抛出
  const verified = await verifyCredentials(account, input)
  if (!verified) {
    await recordAuthenticationFailure(context, account)
    throw authenticationFailed()
  }
  const session = await completeAuthentication(account, context)
  await resetAccountFailures(account)
  await clearThisVisitorFailureBucket(context)
  return session
}

一手参考资料

评论 · 0

还没有评论留下第一句经过思考的话。

游客评论需审核。注册后可直接公开,无需审核。

SHARE / 分享

分享这篇文章

WECHAT / 微信

用微信扫一扫

在手机微信中打开文章后,再从微信右上角分享给朋友或朋友圈。