验证码验证失败后,重试机制必须按照“失败次数递增锁定、多维度叠加风控”的原则来控制,否则任何固定次数的不限时重试都会被暴力破解工具利用。防护的关键不在于验证码本身多复杂,而在于失败后的每一次重试,系统能不能识别出“这是人还是机器”。
为什么验证码重试会成为爆破的突破口
很多开发者把精力全花在验证码生成算法上,却忽略了重试接口才是攻击者真正下功夫的地方,图形验证码、滑块验证码甚至短信验证码,本质上都是一个“验证接口”,攻击者拿不到正确结果,就反复提交、循环枚举,直到撞对一次,行业共识认为,接口层面缺乏重试控制的验证码形同虚设,验证码的防爆破能力不取决于生成端,而取决于校验端的容错设计,常见攻击路径是:攻击者抓包拿到validate接口,脚本化循环提交,每次只更换验证码值或者token,一旦发现返回成功就立即放行,这类攻击的特征是短时间内请求密度极高、验证码错误率接近100%,但服务器端如果没有记录失败次数,就毫无感知。
验证码一直验证失败,到底该重试几次才算安全
这里的核心设计原则是失败次数必须与服务端会话状态绑定,不能只靠前端限制,前端按钮置灰、倒计时这类手段只能拦住普通用户,拦不住任何脚本。
单次会话内的失败上限设置
以图形验证码为例,行业内比较稳妥的做法是单IP单会话内允许连续失败3到5次,超过后强制进入冷却期,冷却期内请求会直接返回“尝试次数过多”,即使输入的验证码是正确的也不予通过,原因很简单:真人连续输错几次数次后通常会停下来看看,不会在一秒内连刷十次。
重试间隔的阶梯式递增
失败后的重试不应该给一个固定的冷却时间,而是按失败次数阶梯式拉长:
- 首次失败至第三次失败:间隔保持正常,不打扰用户体验
- 第四次到第六次失败:每次失败后必须等待30秒才能发起新请求
- 第七次到第九次失败:等待时间拉长到5分钟
- 第十次及以上失败:直接锁定该会话,转入人工审核或强制换用其他验证方式
这种设计的价值在于,人机成本快速抬高,对真人来说前几次失败的等待几乎无感,但脚本若想暴力枚举1000次验证码,累计等待时间会膨胀到不可接受。
失败计数不能只数错误验证码
注意计数范围:只要发起了验证码校验请求,不管结果是成功还是失败,都应当算作一次重试,攻击者可以用“先猜一次、猜不中再换验证码”的方式绕过单纯的错误计数,所以必须统计的是校验接口的整体调用次数,而不是错误次数。
图形验证码防爆破方案:锁定策略怎么设计才管用
锁定策略不能一刀切,按照风险等级分而治之才是常见做法。
按IP维度的锁定
当同一IP在短时间内对一个验证码接口发起大量请求,触发频率上限后,对该IP进行一段时间的验证码服务降级,比如直接拒绝提供服务,或者只返回极难识别的复杂验证码,需要注意的是,出口IP经常是共享的,比如公司网络或校园网,锁定IP容易误伤真实用户,因此IP锁定建议只用在极高频率的场景(比如超过每分钟60次),且锁定时间不宜过长,几分钟到半小时比较合适。
按设备指纹维度的锁定
设备指纹比IP更精确,同一个浏览器或设备上连续失败多次,即使更换IP也会被识别出来,现代前端可以通过Canvas指纹、WebGL信息、时区、字体列表、UA头组合出稳定的设备标识,把这个维度纳入重试控制后,攻击者清cookie换IP的绕行手段就失效了。
账号维度的锁定
如果验证码是在登录或注册场景下使用,那么账号维度是优先级最高的锁定维度。验证码错误次数太多被锁定多久取决于业务风险等级,普通业务建议首次锁定15分钟,累计触发多次后锁定时间递增至1小时、12小时甚至24小时,这里有一个细节:锁定对象应该是“账号+验证码校验动作”的组合,不能因为验证码连续失败而直接封禁账号,否则攻击者用受害者账号名批量触发,就变成了变相封号攻击。
不同风险等级下的验证码升级策略
锁定之外还可以考虑升级验证码形态,比如当一个会话连续失败三次后,图形验证码自动替换为滑块验证或点选验证,大幅度提升机器识别的成本,相比单纯加长冷却时间,这种策略对用户体验的伤害更小,真人只会觉得“验证变难了一点”,而脚本则直接卡在行为识别环节。
分布式场景下控制验证码重试的落地细节
单机部署时用内存Map就能记录失败次数,但一旦做了负载均衡,请求会分散到多台机器上,单机内存计数就失效了,这时候需要把计数放到集中式存储里。
用Redis实现集中式计数
推荐用Redis的INCR和EXPIRE命令实现滑动窗口计数,每个维度一个key,例如captcha:fail:{ip}、captcha:fail:{deviceId}、captcha:fail:{account},设置合适的过期时间,每次校验失败就自增一次,超过阈值直接拦截,Redis的原子性保证并发场景下计数不会错乱,统计数据显示,相当一部分中小型网站被爆破成功,并不是验证码算法弱,而是重试计数存在并发缝隙,攻击者用多线程同时提交,绕过了本地计数。
异步落库防止服务重启丢失计数
Redis数据在服务重启后可能丢失,如果被爆破的是重要业务(比如管理后台登录、支付确认页),建议同步把失败记录写入日志或数据库,这个动作对用户无感,但在安全事件回溯时价值巨大。
网关层前置限流
不要把重试控制的全部逻辑都放在业务代码里,在Nginx或API网关层面,可以直接对验证码校验接口做限流配置,限制单IP的每秒请求数,这样即使业务代码出现漏洞,网关仍能挡掉大部分恶意流量。
控制重试时容易踩的坑
返回值不能暴露剩余次数
触发锁定后,接口返回的提示应当统一为“验证码错误或已过期”,不要明确告诉攻击者“还剩两次机会”、“锁定剩余时间55分钟”,后者会帮助攻击者调整攻击节奏,也能用来做账号存在性探测。
前端校验与后端校验必须双写
有些团队为了减少服务器压力,先在前端做一次验证码校验,通过后才向后端发请求,这种做法非常危险,攻击者完全可以绕过前端,直接向后端发起请求,后端的重试控制逻辑必须独立于前端存在,不能互相信任。
敏感操作验证码通过后也要限制令牌有效期
验证码校验通过后一般会发一个临时令牌,这个令牌的有效期不能太长,业内专家指出,验证码令牌有效期超过10分钟的场景下,中间人重放攻击的成功概率会明显上升,一般建议有效期为2到5分钟,且只能使用一次,用后即焚。
验证码防爆破控制重试的推荐配置参考
| 维度 | 失败阈值 | 锁定时间 | 适用场景 |
|---|---|---|---|
| IP | 15次/5分钟 | 5-10分钟 | 所有场景通用 |
| 设备指纹 | 5次/10分钟 | 30分钟 | 登录注册 |
| 账号 | 5次/10分钟 | 15分钟起递增 | 账号体系 |
| 验证码类型 | 3次连续失败 | 升级验证方式 | 高风险操作 |
这个配置不是唯一答案,但它是经过实战检验的基线,正式上线前建议用压测工具模拟高频错误提交,确认锁定后的响应符合预期。
验证码重试逻辑的日常监控
重试控制配好了,还要能发现问题,重点关注两个指标:验证码校验接口的整体错误率和单IP的请求分布,如果错误率突然从5%飙升到80%,说明很可能有脚本在跑字典,如果某个IP的请求频次曲线呈均匀间隔分布,那是典型脚本特征真人操作的时间间隔是随机的,机器才那么均匀,日志记录时,除了通用的时间、IP、UA,建议把验证码的图片ID或token一并记录下来,便于追溯到底哪一批验证码被重放攻击了。
Q&A:验证码错误次数太多被锁定多久
验证码错误次数太多被锁定多久?
锁定时间取决于业务对安全性和易用性的平衡,常规业务首次锁定15分钟,叠加触发后最长可达到24小时,你在登录页面看到“操作频繁,请稍后再试”的提示,通常就是触发了这个锁定,如果是金融、支付等高安全要求场景,首次锁定可能只有5分钟,但触发第二次后就跳到12小时,阶梯非常陡。
为什么验证码输入正确了,还是提示验证失败?
最常见原因是前端页面过期,验证码本身有有效期,大部分图形验证码的有效期是2到5分钟,超时后即使输入正确也会判失败,其次就是触发了上面提到的重试锁定:系统检测到你的请求频率过高,进入了冷却期,这种状态下无论输入什么内容都会直接拒绝,建议刷新页面重新获取新的验证码,等待冷却期结束后再试。
直接放弃重试、刷新验证码,能绕过防爆破限制吗?
不能,刷新验证码这个动作本身也需要调用接口,同样会被计数,大多数防爆破设计会把“获取验证码”和“校验验证码”放在同一个限流逻辑里,短时间内频繁刷新验证码,本身就是一个高风险信号,会触发更强的风控策略,所以正常操作是:失败后看清楚再输,不要盲目反复刷新。
首发原创文章,作者:王坚,如若转载,请注明出处:https://idctop.com/article/635475.html





