【微码】Java 基于 Redis 实现登录频率限制

登录限制:5 分钟限制尝试登录 3 次,错误次数超过限制进行用户锁定
博客:https://www.emanjusaka.com
博客园:https://www.cnblogs.com/emanjusaka
公众号:emanjusaka的编程栈
我们在实现登录功能时,经常会有这个需求。
5 分钟内限制尝试登录 3 次,错误次数超过限制就要对该用户进行锁定。一段时间内无法再次进行登录。
这个场景需求可以怎样实现?
通过分析可以把这个需求分成两部分:
- 限制用户 5 分钟内最多尝试登录 3 次
- 对用户进行锁定
限制用户 5 分钟内最多尝试登录 3 次
实现这个需求可以考虑使用滑动窗口限流。
对用户进行锁定
把被锁定的用户保存在Redis中并设置一个合理的超时时间。
代码实现
package com.emanjusaka.loginratelimiter;
import lombok.AllArgsConstructor;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
/**
* @Author emanjusaka
* @Date 2025/5/27 10:43
* @Version 1.0
*/
@AllArgsConstructor
public class SlidingWindowRateLimiter {
private String key;
private int limit = 3; // 限制请求次数最大 3 次
private int lockTime; // 锁定用户的时间,单位为秒
private StringRedisTemplate stringRedisTemplate;
public boolean allowRequest() {
// 当前时间戳,单位:毫秒
long currentTime = System.currentTimeMillis();
// 锁定键的名称(锁定的用户)
String lockKey = "lock:" + key;
// 检查用户是否已被锁定
if (Boolean.TRUE.equals(stringRedisTemplate.hasKey(lockKey))) {
return false; // 用户已被锁定,返回 false
}
// 使用Lua脚本来确保原子性操作
String luaScript = "local window_start = ARGV[1] - 300000\n" + // 计算5分钟的起始时间
"redis.call('ZREMRANGEBYSCORE', KEYS[1], '-inf', window_start)\n" + // 清理过期的请求
"local current_requests = redis.call('ZCARD', KEYS[1])\n" + // 获取当前请求次数
"if current_requests < tonumber(ARGV[2]) then\n" + // 如果请求次数小于限制
" redis.call('ZADD', KEYS[1], ARGV[1], ARGV[1])\n" + // 添加当前请求时间
" return 1\n" + // 允许请求
"else\n" +
" redis.call('SET', 'lock:'..KEYS[1], 1, 'EX', tonumber(ARGV[3]))\n" + // 锁定用户
" return 0\n" + // 拒绝请求
"end";
// 调用 Lua 脚本进行原子操作
Long result = stringRedisTemplate.execute(
new DefaultRedisScript<>(luaScript, Long.class),
java.util.Collections.singletonList(key),
String.valueOf(currentTime),
String.valueOf(limit),
String.valueOf(lockTime)
);
// 返回操作结果
return result != null && result == 1;
}
}
-
ZREMRANGEBYSCORE
命令会移除当前时间窗口外的记录,确保统计的是最近 5 分钟内的登录尝试次数。 -
滑动窗口:使用 Redis 有序集合(ZADD)记录每次登录尝试的时间戳,过期的记录会被自动清理。
-
允许请求:如果在 5 分钟内的请求次数没有超过限制,脚本会将当前请求的时间戳添加到 Redis 的有序集合中,并返回 1,表示允许请求。
-
拒绝请求:如果用户在 5 分钟内的请求次数超过限制,脚本会设置用户的锁定键,并返回 0,表示拒绝请求。
-
用户锁定:在 Lua 脚本中,我们使用 SET 命令在 Redis 中设置一个名为 lock:<user_id> 的键,表示该用户已经被锁定。当尝试次数超过限制时,设置这个锁,并给它一个过期时间(lockTime)。
代码调用
package com.emanjusaka.loginratelimiter;
import jakarta.annotation.Resource;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.redis.core.StringRedisTemplate;
@SpringBootTest
class LoginRateLimiterApplicationTests {
@Resource
private StringRedisTemplate stringRedisTemplate;
@Test
void contextLoads() {
SlidingWindowRateLimiter slidingWindowRateLimiter = new SlidingWindowRateLimiter("1001", 3, 100, stringRedisTemplate);
System.out.println(slidingWindowRateLimiter.allowRequest());
}
}
在 5 分钟内调用超过 3 次,该用户将会被锁定 100 秒。假设用户在 T 时刻被锁定 100 秒,之后继续请求会有两种情况发生:
- 情况 1:如果用户在 T+100 秒后请求,且 ZSet 中仍有≥4 条 5 分钟内的记录,则会再次被锁定 100 秒。
- 情况 2:如果用户在 T+300 秒后请求(5 分钟后),此时 ZSet 中最早的记录已被清理,若剩余记录不足 3 条,则请求通过。
在 Lua 脚本中,当检测到用户请求次数超过限制时,可以新增一行代码:
redis.call('DEL', KEYS[1])
这样在用户被锁定前,就会删除整个 ZSet,从而清空用户的所有请求记录。当 100 秒锁定时间结束后,用户再次请求时,ZSet 将是一个全新的空集合,计数会从零开始,不会因为之前的请求记录而立即再次触发限流。
这块怎么处理就看具体需求,根据需求去调整。