Redis滑动窗口做登录限流

这里实现一分钟内只允许5次登录,废话不多说直接上代码:

1.使用 Lua 脚本 将“清理过期数据 → 统计计数 → 判断是否超限 → 添加新记录”四个操作封装为一个原子操作,避免高并发下 count 与 add 之间的竞争条件。
-- KEYS[1] : 限流key,例如 "login:limit:phone:138****0000"
-- ARGV[1] : 当前时间戳(毫秒)
-- ARGV[2] : 窗口大小(毫秒)
-- ARGV[3] : 限流阈值(最大请求次数)
-- ARGV[4] : 本次请求的唯一标识(例如时间戳+随机数)

local window_start = tonumber(ARGV[1]) - tonumber(ARGV[2])
-- 1. 删除窗口外的旧数据
redis.call('ZREMRANGEBYSCORE', KEYS[1], 0, window_start)
-- 2. 统计窗口内当前请求数
local current_count = redis.call('ZCARD', KEYS[1])
-- 3. 判断是否超过阈值
if current_count < tonumber(ARGV[3]) then
    -- 4. 添加本次请求记录
    redis.call('ZADD', KEYS[1], ARGV[1], ARGV[4])
    -- 5. 设置 key 过期时间(窗口大小 + 1秒缓冲)
    redis.call('EXPIRE', KEYS[1], math.ceil(tonumber(ARGV[2]) / 1000) + 1)
    return 1   -- 放行
else
    return 0   -- 限流
end
2.新建一个limit包,编写服务如下,也可以写在service实现类中:
package com.hmdp.limit;

import org.springframework.core.io.ClassPathResource;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.scripting.support.ResourceScriptSource;
import org.springframework.stereotype.Service;

import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import java.util.Collections;
import java.util.UUID;

@Service
public class LoginLimitService {


    @Resource
    private StringRedisTemplate stringRedisTemplate;

    //定义一个redis lua脚本,返回long类型数据
    private DefaultRedisScript<Long> loginLimitScript;

    private static final long WINDOW_SIZE_MS = 60 * 1000L; // 1分钟  窗口
    private static final int LIMIT_COUNT = 5;

    @PostConstruct
    public void init() {
        loginLimitScript = new DefaultRedisScript<>();
        // 加载 resources/lua/login_limit.lua
        loginLimitScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("login_limit.lua")));
        loginLimitScript.setResultType(Long.class);
    }

    public boolean tryAcquire(String phoneNumber) {
        String key = "login:limit:phone:" + phoneNumber;
        long now = System.currentTimeMillis();  //获取当前系统时间的时间戳
        String member = now + "_" + UUID.randomUUID().toString();    //在zset中member如果一样 后写的会覆盖前写的  保证member的唯一

        Long result = stringRedisTemplate.execute(
                loginLimitScript,
                Collections.singletonList(key),
                String.valueOf(now),
                String.valueOf(WINDOW_SIZE_MS),
                String.valueOf(LIMIT_COUNT),
                member
        );
        return result != null && result == 1L;
    }
}

注意这里StringRedisTemplate需要配置序列化器

3.登录接口:
   @PostMapping("/login")
    public Result login(@RequestBody LoginFormDTO loginForm, HttpSession session, HttpServletRequest request){
        String phone =loginForm.getPhone();
        //  实现登录功能
        if ( phone== null){
            phone = IpUtils.getIp( request);  //拿到ip 作为手机号
        }
        //添加滑动窗口进行登录限流   每一分钟 最多5次
        if (!loginLimitService.tryAcquire(phone)) {
            return Result.fail("登录尝试过于频繁,请稍后再试");
        }

        return userService.login(loginForm, session);
    }
4.如果请求手机号是空的,拿到请求ip做key,在config包下添加iputils如下:
package com.hmdp.utils;

import javax.servlet.http.HttpServletRequest;

//获取ip的工具类
public class IpUtils {

    public static String getIp(HttpServletRequest request) {
        String ip = request.getHeader("X-Forwarded-For");
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("Proxy-Client-IP");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("WL-Proxy-Client-IP");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getRemoteAddr();
        }
        // 取第一个IP
        if (ip != null && ip.contains(",")) {
            ip = ip.split(",")[0].trim();
        }
        return ip;
    }
}

当然如果手机号码是强校验也可以省略ip来做key

5.postman做测试:在连续点击5次登录请求之后,可以看到直接返回了登录过于频繁

注意401  要带请求token

6.redis中窗口如图所示:

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值