业务侧限制登录尝试次数是挡掉暴力破解最直接、最有效的手段,没有之一,与其依赖防火墙或WAF在外部层层设卡,不如在应用层把登录这扇门锁死。
为什么暴力破解总盯着登录接口
黑客攻击一个系统,很少硬碰硬地怼代码漏洞,多数是从最笨也最有效的登录口下手,业内有个共识:相当一部分安全事件并不是被什么高级漏洞击穿的,而是被简单的密码爆破撞开的。
暴力破解的逻辑简单粗暴:拿一个常用密码字典,对着你的登录接口一遍遍地试,admin/123456”这种组合,如果系统没有任何限制,一次跑几万次请求也就是几分钟的事,特别是那些没有验证码、没有频率控制的旧系统,几乎等于把门敞开着。
传统安全设备不是没有防护能力,但网络层的WAF规则往往滞后,而且容易被绕过,业务侧做限制的逻辑完全不同:你不是在我门外试探,你是要进我的屋子,那我就规定好一小时内只能试错五次,超了直接锁门。 这种力度,和外面挡子弹是两个概念。
登录尝试次数限制的完整实现方案
先理清楚:到底要限制什么
很多开发人员上手就写“失败五次锁账号”,但这个思路有问题,至少不全面,业务侧的限制要分三个维度:
- 单账号维度:同一个用户名,短时间内连续失败N次就锁定,防止针对特定账号的定向爆破。
- 单IP维度:同一个来源IP,不管试哪个账号,总失败次数达到阈值就暂时封禁,防止攻击者撒网式地拿一堆账号轮着试。
- 设备指纹维度:同一个浏览器指纹或客户端标识,即便更换IP,也能被识别出来,这个维度能有效对抗攻击者用代理池换IP绕过限制。
三个维度缺一个,就容易被钻空子,单限制账号,攻击者可以分布式换IP;单限制IP,攻击者会锁定一个账号慢慢试;两者结合再加设备指纹,才算严实。
锁定策略的两种主流模式
固定窗口模式。 这是最基础的实现:设定一个计数器,比如15分钟内允许失败5次,达到就锁定30分钟,代码逻辑简单,用一张表或Redis的计数器就能完成。
滑动窗口模式。 固定窗口有个问题:攻击者卡在窗口边界操作,比如第14分59秒试5次,过了零点又能试5次,实际频率远超预期,滑动窗口则用Redis的ZSET记录每个时间点的失败记录,每次判断“最近15分钟失败了几次”,效果好,代价是内存消耗略高。
行业建议,初期没必要上滑动窗口,固定窗口已经能挡住95%的脚本攻击。 把账户锁定通知、解锁流程做完善,实战效果远比算法复杂度重要,毕竟攻击者要的是效率,一个登录口试不了几次就锁了,他自然换目标。
阈值设置:多大算合理,多小算误伤
阈值设置是全篇文章里最容易扯皮的地方,设太小,用户手滑输错两三次密码就被锁,客服电话被打爆;设太大,跟没设一样。
引用一个常规经验值:
| 场景 | 短期阈值 | 锁定时长 | 说明 |
|---|---|---|---|
| 普通用户登录 | 15分钟内5-8次失败 | 15-30分钟 | 兼顾体验与安全 |
| 管理员后台登录 | 10分钟内3-5次失败 | 30-60分钟 | 管理员账号价值高,宁严勿松 |
| 接口型认证(API Key) | 5分钟内3次失败 | 1小时 | 程序化调用更严格 |
| 支付/提现等敏感操作 | 10分钟内3次失败 | 24小时 | 涉及资金,必须最严 |
锁定方式也要分层。 第一次触发只锁账号,第二次触发同时锁IP,第三次直接进入人工审核流程(要求短信验证码重置密码),这种阶梯式设计,比一刀切锁死更合理。
数据存储与分布式场景下的实现
单机部署简单,用内存缓存(Caffeine/Guava)就够,但现在的业务系统基本是分布式多节点,就必须引入集中式缓存。
推荐用Redis实现,步骤如下:
- 用户登录失败时,以
login:fail:username:{userName}为key,执行INCR操作。 - 第一次失败时,设置过期时间,比如15分钟。
- 每次失败后检查计数值,达到阈值则设置锁定key,如
login:lock:username:{userName},过期时间设为锁定时长。 - 同理,对IP做
login:fail:ip:{ip}的计数。
用Lua脚本把“检查-加一-判断-锁定”四个动作原子化,避免高并发下超发,这个细节能绕开竞态条件导致的防线失效,也是从业者踩过坑后的共识。
-- 简易Lua脚本示例 local failKey = KEYS[1] local lockKey = KEYS[2] local maxFail = tonumber(ARGV[1]) local lockTimeout = tonumber(ARGV[2]) local failCount = redis.call('INCR', failKey) redis.call('EXPIRE', failKey, ARGV[3]) if failCount >= maxFail then redis.call('SETEX', lockKey, lockTimeout, '1') return 1 end return 0
这段逻辑挂在登录接口最前面,先检查lockKey是否存在,存在就直接拒绝,不再继续验证密码。
验证码:第二道防线怎么配合
限制次数不等于一定要牺牲体验。加入验证码机制,才能让正常用户顺畅登录,让脚本无路可走。
常见的编排方式有两种:
- 触发式:前2次密码错误不弹验证码,第3次开始强制要求输入图形验证码或滑块验证,正常用户基本无感,脚本则被挡在验证码外面。
- 必选式:每次登录都要验证码,安全性最高,但转化率会有一定损耗,适合管理后台或金融类场景。
从成本角度看,触发式是多数B端业务系统的首选方案,安全性和用户体验的平衡点最好。
绕过限制的常见手法与对策
IP伪造与代理池问题
攻击者意识到IP被限后,第一反应就是换IP,这里有个常识需要区分:HTTP头里的X-Forwarded-For是完全可以伪造的,如果你的后端直接用这个头取IP,限制等于白做。
正确做法是取TCP连接层的真实IP,或经过Nginx时用$remote_addr配合proxy_protocol协议传递,用高匿代理池换IP的行为,只能靠设备指纹和行为特征来兜底。
批量撞库与分布式爆破
撞库的特点是用超大字典、极慢速度去试,比如每小时只发200个请求,完美绕过低频限制,这时候需要依赖风控引擎做综合判断,包括:
- 同一IP段内的集中失败率
- 同一UserAgent大批量出现
- 登录时间的异常分散模式
- 密码输入速度(真人不可能每间隔几秒精确输入一次)
锁定机制本身被利用的风险
这是许多开发者忽略的陷阱:攻击者故意输错密码,把你的正常用户账号锁掉,制造的是另一种攻击恶意锁定。 比如大半夜把你竞争对手所有员工的账号全部试错五次,第二天公司全员无法登录,业务直接瘫痪。
对策也很简单:
- 失败计数针对“来源IP”而非“账号”,即同一IP下的大量失败尝试只影响该IP。
- 或对账号锁定但不告知锁定状态,统一提示“用户名或密码错误”,让攻击者无法确认是否命中,也无法确定是密码错还是账号锁。
登录保护也要考虑解锁流程与用户体验
限制登录次数只是前半段,后半段是解锁。
最差的解锁方式:等30分钟自动解锁,无人工干预入口,用户密码忘了只能干瞪眼,体验极差。
合理的解锁方式不止一种,推荐组合:
- 自助解锁:通过注册手机号或邮箱验证身份,即时解锁并重置密码。
- 人工客服解锁:用户致电客服,核实身份后由后台手动解锁。
- 邮件通知解锁:锁定发生时自动发邮件,附带安全验证链接,用户点击完成身份验证即解锁。
从部署顺序来看,先把自动计数和锁定做出来,再补验证码,最后完善解锁流程,这属于成本最低、见效最快的安全改造路线。
登录失败的日志与监控同样关键
没有日志的登录限制等于盲人摸象,如果攻击者一直在尝试破解,而你只闷头封IP,不去看日志,那永远不知道系统正被针对,至少要做到:
- 记录每次登录失败的时间戳、用户名、来源IP、请求头信息,存够180天以上
- 在监控大屏或告警规则里设定“登录失败率异常飙升”触发通知
- 每天生成一份登录失败摘要报表,给安全负责人过目
需要指出的误区是,大量企业采购了昂贵的安全设备,却连最简单的应用日志分析都没做。 别把希望寄托在设备上,日志和告警才是每天实在干活的东西。
常见问题快速解答
登录限流和验证码是二选一的关系吗?
不是,限流(次数限制)解决的是“试太多次”,验证码解决的是“是不是人在操作”,两者属于互补关系,正确的组合是:限流作为兜底,验证码作为交互门槛,配合使用才能应对不同层级的攻击,只做验证码,打码平台可以绕过;只做限流,慢速爆破防不住。
限流阈值设置多少才算稳妥?
没有通用的标准数值,取决于用户规模和业务性质,面向大众用户的C端产品,阈值可以放宽到15分钟10次;B端后台和运维平台,建议10分钟3次起步。阈值设置后需要观察两周的生产日志,如果经常误锁用户,再适度上调。
锁定账号后如何保证正常用户不被攻击者恶意搞坏?
采用“IP维度计数优先于账号维度计数”的策略,同一个IP的失败次数先触发限制,而非马上锁定账号,只有该账号在多个不同IP下持续失败,才触发账号级锁定,这样攻击者用单一IP爆破时,受惩罚的是攻击者自己的IP,正常用户的账号反而受到保护。
首发原创文章,作者:王坚,如若转载,请注明出处:https://idctop.com/article/651578.html





