
你是不是也遇到过登录接口的 “糟心事”?
作为互联网开发,你有没有过这种经历?凌晨两点被运维同事的电话叫醒,说线上登录接口突然涌入上千次错误请求,数据库连接池被占满,正常用户登不上去;或者排查日志时发现,某个 IP 在 10 分钟内连续输错 20 次密码,明显是恶意破解 —— 最后不仅要紧急扩容修复,还得写故障报告反思 “为什么没做好防护”。
实则不用慌,这种 “登录接口暴力破解” 问题,90% 的后端团队都遇到过。今天就跟你聊个简单又高效的解决方案:用 Lua 脚本结合 Redis 实现 IP 锁定功能,代码能直接复制到项目里用,后来再遇到类似情况,你再也不用熬夜背锅!
为什么 IP 锁定是防护暴力破解的 “最优解”?
在说具体实现前,先跟你理清一个逻辑:为什么要选 “IP 锁定”,而不是其他方案?
第一,咱们得明确暴力破解的核心逻辑 —— 黑客通过工具批量生成账号密码,高频次调用登录接口,靠概率撞出正确信息。针对这种攻击,常见的防护手段有 3 种:
- 验证码机制:虽然能拦截机器请求,但会增加正常用户的操作成本,尤其移动端登录体验会变差;
- 账号锁定:列如 “输错 5 次密码锁定账号 1 小时”,但如果黑客针对的是未注册账号,或者用大量小号测试,这个方案就失效了;
- IP 锁定:聚焦 “请求来源”,不管用哪个账号,只要同一 IP 在短时间内错误次数超标就锁定,既能精准拦截恶意请求,又不影响正常用户,还能避免 “小号攻击” 的漏洞。
再结合咱们开发场景说个细节:许多团队用 Java 写登录接口时,会直接在代码里写 IP 计数逻辑,但这样有个问题 —— 分布式部署时,多台服务器的计数无法同步,可能导致 IP 锁定失效。而 Redis 是分布式环境下的 “共享内存”,能保证计数准确;再加上 Lua 脚本的 “原子性”,能避免 “并发计数误差”,这也是咱们选这个方案的关键缘由。
3 步实现 IP 锁定,代码直接复制能用
接下来就是最核心的实操环节,我会分 “Lua 脚本编写”“Redis 执行测试”“Java 项目集成” 3 步来写,每一步都附完整代码和注释,你跟着做就能搞定。
第一步:编写 Lua 脚本,实现 IP 计数与锁定逻辑
咱们先定义核心规则:同一 IP 在 10 分钟内,登录错误次数超过 5 次,就锁定该 IP1 小时(3600 秒)。Lua 脚本的优势是能把 “计数 + 判断 + 锁定” 写成一个原子操作,避免多线程下的计数混乱。
-- 1. 定义参数:从外部传入3个值
-- KEYS[1]:IP作为Redis的key,格式提议为"login:ip:192.168.1.1"
-- ARGV[1]:错误次数阈值(这里传5)
-- ARGV[2]:计数过期时间(10分钟=600秒)
-- ARGV[3]:IP锁定时间(1小时=3600秒)
local ipKey = KEYS[1]
local errorThreshold = tonumber(ARGV[1])
local countExpire = tonumber(ARGV[2])
local lockExpire = tonumber(ARGV[3])
-- 2. 判断IP是否已被锁定:检查是否存在"login:lock:IP"的key
local isLocked = redis.call("EXISTS", "login:lock:" .. ipKey)
if isLocked == 1 then
-- 返回-1表明已锁定,附带剩余锁定时间
local remainTime = redis.call("TTL", "login:lock:" .. ipKey)
return { -1, remainTime }
end
-- 3. 未锁定则计数:获取当前错误次数,不存在则返回0
local errorCount = redis.call("GET", ipKey)
if errorCount == false then
errorCount = 0
else
errorCount = tonumber(errorCount)
end
-- 4. 判断计数是否超过阈值
if errorCount >= errorThreshold then
-- 超过阈值,添加锁定key,设置过期时间
redis.call("SET", "login:lock:" .. ipKey, 1, "EX", lockExpire)
-- 返回-2表明刚被锁定,附带锁定时长
return { -2, lockExpire }
else
-- 未超过阈值,计数+1,更新过期时间
errorCount = errorCount + 1
redis.call("SET", ipKey, errorCount, "EX", countExpire)
-- 返回当前错误次数和剩余计数时间
local remainCountTime = redis.call("TTL", ipKey)
return { errorCount, remainCountTime }
end
第二步:Redis 执行测试,验证脚本逻辑
脚本写好后,咱们先在 Redis 里测试一下,确保逻辑没问题。这里用 Redis CLI 举例,你也可以用 Redis Desktop Manager 等工具。
模拟第一次错误请求:执行脚本,IP 为 192.168.1.1,阈值 5,计数过期 600 秒,锁定过期 3600 秒
redis-cli EVAL "上面的Lua脚本" 1 "login:ip:192.168.1.1" 5 600 3600
返回结果:1 600 → 表明当前错误次数 1,剩余计数时间 600 秒,符合预期。
模拟第 5 次错误请求:连续执行到第 5 次,会返回5 600;再执行第 6 次时,会返回-2 3600 → 表明刚被锁定,锁定 3600 秒。
测试锁定状态:此时再执行脚本,会返回-1 3598(具体剩余时间随测试时间变化)→ 表明已锁定,符合预期。
第三步:Java 项目集成,对接登录接口
最后一步,把这个功能集成到 Java 登录接口里。这里用 Spring Boot + Jedis 举例,其他框架逻辑类似。
第一,添加 Jedis 依赖(如果用 Spring Data Redis 也可以,核心是调用 Lua 脚本):
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>4.4.6</version>
</dependency>
然后,写一个 IP 锁定工具类:
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Component
public class IpLockUtil {
// 注入Redis连接池,实际项目中提议用配置类管理
@Autowired
private JedisPool jedisPool;
// Lua脚本内容,直接复制上面写的脚本
private static final String IP_LOCK_LUA = "local ipKey = KEYS[1]...(完整Lua脚本)";
/**
* 处理IP登录错误计数与锁定
* @param ip 用户IP
* @return 结果数组:[状态码, 附加信息]
* 状态码:-1=已锁定,-2=刚锁定,正数=当前错误次数
* 附加信息:状态码-1时为剩余锁定时间,-2时为锁定时长,正数时为剩余计数时间
*/
public Object[] handleLoginError(String ip) {
try (Jedis jedis = jedisPool.getResource()) {
// 1. 构造Redis的key:login:ip:192.168.1.1
String redisKey = "login:ip:" + ip;
// 2. 调用Lua脚本,传入参数:阈值5,计数过期600秒,锁定过期3600秒
return jedis.eval(IP_LOCK_LUA, 1, redisKey, "5", "600", "3600");
} catch (Exception e) {
// 异常处理:提议打印日志,避免影响登录主流程
e.printStackTrace();
// 返回null表明处理失败,实际项目中可根据需求调整
return null;
}
}
}
最后,在登录接口里调用这个工具类:
@PostMapping("/login")
public Result login(@RequestParam String username,
@RequestParam String password,
HttpServletRequest request) {
// 1. 获取用户IP(实际项目中注意处理代理IP,这里简化)
String userIp = request.getRemoteAddr();
// 2. 调用IP锁定工具类
Object[] lockResult = ipLockUtil.handleLoginError(userIp);
if (lockResult == null) {
return Result.error("系统异常,请稍后再试");
}
// 3. 处理返回结果
int status = Integer.parseInt(lockResult[0].toString());
if (status == -1) {
int remainTime = Integer.parseInt(lockResult[1].toString());
return Result.error("该IP已被锁定,剩余" + remainTime + "秒后可尝试");
} else if (status == -2) {
int lockTime = Integer.parseInt(lockResult[1].toString());
return Result.error("错误次数过多,IP已锁定" + lockTime + "秒");
}
// 4. 正常登录逻辑:验证账号密码(此处省略)
boolean loginSuccess = userService.verify(username, password);
if (loginSuccess) {
// 登录成功:清除该IP的错误计数
try (Jedis jedis = jedisPool.getResource()) {
jedis.del("login:ip:" + userIp);
}
return Result.success("登录成功");
} else {
// 登录失败:返回当前错误次数
return Result.error("账号或密码错误,已错误" + status + "次,共5次机会");
}
}
总结呼吁:把防护做在前面,比事后补救更重大
看到这里,你应该已经掌握了 IP 锁定的完整实现方案 —— 从 Lua 脚本到 Redis 测试,再到 Java 集成,每一步都有具体代码,你今天下班前就能加到项目里。
最后想跟你说句心里话:作为后端开发,咱们不仅要实现业务功能,更要思考 “系统安全性”。像登录接口暴力破解这种问题,等到出了故障再修复,不如提前做好防护。这个 IP 锁定方案虽然简单,但能解决 80% 的恶意攻击场景,而且性能损耗极低 ——Redis 的 QPS 能轻松支撑几万请求,Lua 脚本执行时间不到 1 毫秒,完全不用担心影响接口性能。
如果你在集成过程中遇到问题,列如 “分布式环境下 IP 获取不准”“Redis 集群执行 Lua 脚本报错”,欢迎在评论区留言讨论;如果你的项目里有更好的防护方案,也请分享出来,咱们一起帮更多后端同事少踩坑、少背锅!
收藏了,感谢分享