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

本文发布于 2025年05月27日,阅读 11 次,点赞 0 次,归类于 微码
【微码】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 将是一个全新的空集合,计数会从零开始,不会因为之前的请求记录而立即再次触发限流。

这块怎么处理就看具体需求,根据需求去调整。

本篇完
下一篇: Artalk 评论系统部署配置和避坑指南