Redis实现IP限流的两种方式详解

embedded/2024/12/22 1:56:10/

gateway网关ip限流

通过reids实现

  1. 限流的流程图

    image-20240809111850316

  2. 在配置文件配置限流参数

    blackIP:# ip 连续请求的次数continue-counts: ${counts:3}# ip 判断的时间间隔,单位:秒time-interval: ${interval:20}# 限制的时间,单位:秒limit-time: ${time:30}
    
  3. 编写全局过滤器类

    package com.ajie.gateway.filter;import com.ajie.common.enums.ResponseStatusEnum;
    import com.ajie.common.result.GraceJSONResult;
    import com.ajie.common.utils.CollUtils;
    import com.ajie.common.utils.IPUtil;
    import com.ajie.common.utils.JsonUtils;
    import com.ajie.common.utils.RedisUtil;
    import io.netty.handler.codec.http.HttpHeaderNames;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.beans.factory.annotation.Value;
    import org.springframework.cloud.gateway.filter.GatewayFilterChain;
    import org.springframework.cloud.gateway.filter.GlobalFilter;
    import org.springframework.core.Ordered;
    import org.springframework.core.io.buffer.DataBuffer;
    import org.springframework.http.HttpStatus;
    import org.springframework.http.server.reactive.ServerHttpRequest;
    import org.springframework.http.server.reactive.ServerHttpResponse;
    import org.springframework.stereotype.Component;
    import org.springframework.util.AntPathMatcher;
    import org.springframework.util.MimeTypeUtils;
    import org.springframework.web.server.ServerWebExchange;
    import reactor.core.publisher.Mono;import java.nio.charset.StandardCharsets;
    import java.util.List;
    import java.util.concurrent.TimeUnit;/*** @Description:* @Author: ajie*/
    @Slf4j
    @Component
    public class IpLimitFilterJwt implements GlobalFilter, Ordered {@Autowiredprivate UrlPathProperties urlPathProperties;@Value("${blackIP.continue-counts}")private Integer continueCounts;@Value("${blackIP.time-interval}")private Integer timeInterval;@Value("${blackIP.limit-time}")private Integer limitTime;private final AntPathMatcher antPathMatcher = new AntPathMatcher();@Overridepublic Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {// 1.获取当前的请求路径String path = exchange.getRequest().getURI().getPath();// 2.获得所有的需要限流的urlList<String> ipLimitUrls = urlPathProperties.getIpLimitUrls();// 3.校验并且排除excludeListif (CollUtils.isNotEmpty(ipLimitUrls)) {for (String url : ipLimitUrls) {if (antPathMatcher.matchStart(url, path)) {log.warn("IpLimitFilterJwt--url={}", path);// 进行ip限流return doLimit(exchange, chain);}}}// 默认直接放行return chain.filter(exchange);}private Mono<Void> doLimit(ServerWebExchange exchange, GatewayFilterChain chain) {// 获取真实ipServerHttpRequest request = exchange.getRequest();String ip = IPUtil.getIP(request);/*** 需求:* 判断ip在20秒内请求的次数是否超过3次* 如果超过,则限制访问30秒* 等待30秒以后,才能够恢复访问*/// 正常ipString ipRedisKey = "gateway_ip:" + ip;// 被拦截的黑名单,如果存在,则表示该ip已经被限制访问String ipRedisLimitedKey = "gateway_ip:limit:" + ip;long limitLeftTime = RedisUtil.KeyOps.getExpire(ipRedisLimitedKey);if (limitLeftTime > 0) {return renderErrorMsg(exchange, ResponseStatusEnum.SYSTEM_ERROR_BLACK_IP);}// 在redis中获得ip的累加次数long requestTimes = RedisUtil.StringOps.incrBy(ipRedisKey, 1);// 如果访问次数为1,则表明是第一次访问,在redis设置倒计时if (requestTimes == 1) {RedisUtil.KeyOps.expire(ipRedisKey, timeInterval, TimeUnit.SECONDS);}// 如果访问次数超过限制的次数,直接将该ip存入限制的redis key,并设置限制访问时间if (requestTimes > continueCounts) {// 设置该ip需要被限流的时间RedisUtil.StringOps.setEx(ipRedisLimitedKey, ip, limitTime, TimeUnit.SECONDS);return renderErrorMsg(exchange, ResponseStatusEnum.SYSTEM_ERROR_BLACK_IP);}return chain.filter(exchange);}public Mono<Void> renderErrorMsg(ServerWebExchange exchange, ResponseStatusEnum statusEnum) {// 1.获得responseServerHttpResponse response = exchange.getResponse();// 2.构建jsonResultGraceJSONResult jsonResult = GraceJSONResult.exception(statusEnum);// 3.修改response的code为500response.setStatusCode(HttpStatus.INTERNAL_SERVER_ERROR);// 4.设定header类型if (!response.getHeaders().containsKey("Content-Type")) {response.getHeaders().add(HttpHeaderNames.CONTENT_TYPE.toString(), MimeTypeUtils.APPLICATION_JSON_VALUE);}// 5.转换json并且向response写入数据String jsonStr = JsonUtils.toJsonStr(jsonResult);DataBuffer dataBuffer = response.bufferFactory().wrap(jsonStr.getBytes(StandardCharsets.UTF_8));return response.writeWith(Mono.just(dataBuffer));}@Overridepublic int getOrder() {return 1;}
    }
    

通过Lua+Redis实现

业务流程还是和上图差不多,只不过gateway网关不用再频繁和redis进行交互。整个限流逻辑放在redis层,通过Lua代码嵌套

  1. Lua实现限流的代码

    lua">--[[
    ipRedisLimitedKey:限流的redis key
    ipRedisKey:未被限流的redis key,通过此key计算访问次数
    timeInterval:访问时间间隔,在此时间内,访问到指定次数进行限流
    limitTime:限流的时长
    ]]
    -- 判断当前ip是否已经被限流
    if redis.call("ttl", ipRedisLimitedKey) > 0 thenreturn 1
    end-- 如果没有被限流,就让当前ip在redis中的值累计1
    local requestTimes = redis.call("incrby", ipRedisKey, 1)
    -- 判断累加后的值
    if requestTimes == 1 then-- 如果累加后的值是1,说明是第一次请求,设置一个时间间隔redis.call("expire", ipRedisKey, timeInterval)return 0
    elseif requestTimes > continueCounts then--  如果累加后的值超过了设定的阈值,就对当前ip进行限流redis.call("setex", ipRedisLimitedKey, limitTime, ip)return 1
    end
    
  2. java代码实现Lua和redis的整合

    package com.ajie.gateway.filter;import com.ajie.common.enums.ResponseStatusEnum;
    import com.ajie.common.result.GraceJSONResult;
    import com.ajie.common.utils.CollUtils;
    import com.ajie.common.utils.IPUtil;
    import com.ajie.common.utils.JsonUtils;
    import com.ajie.common.utils.RedisUtil;
    import com.google.common.collect.Lists;
    import io.netty.handler.codec.http.HttpHeaderNames;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.beans.factory.annotation.Value;
    import org.springframework.cloud.gateway.filter.GatewayFilterChain;
    import org.springframework.cloud.gateway.filter.GlobalFilter;
    import org.springframework.core.Ordered;
    import org.springframework.core.io.buffer.DataBuffer;
    import org.springframework.http.HttpStatus;
    import org.springframework.http.server.reactive.ServerHttpRequest;
    import org.springframework.http.server.reactive.ServerHttpResponse;
    import org.springframework.stereotype.Component;
    import org.springframework.util.AntPathMatcher;
    import org.springframework.util.MimeTypeUtils;
    import org.springframework.web.server.ServerWebExchange;
    import reactor.core.publisher.Mono;import java.nio.charset.StandardCharsets;
    import java.util.List;/*** @Description:* @Author: ajie*/
    @Slf4j
    @Component
    public class IpLuaLimitFilterJwt implements GlobalFilter, Ordered {@Autowiredprivate UrlPathProperties urlPathProperties;@Value("${blackIP.continue-counts}")private Integer continueCounts;@Value("${blackIP.time-interval}")private Integer timeInterval;@Value("${blackIP.limit-time}")private Integer limitTime;private final AntPathMatcher antPathMatcher = new AntPathMatcher();@Overridepublic Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {// 1.获取当前的请求路径String path = exchange.getRequest().getURI().getPath();// 2.获得所有的需要限流的urlList<String> ipLimitUrls = urlPathProperties.getIpLimitUrls();// 3.校验并且排除excludeListif (CollUtils.isNotEmpty(ipLimitUrls)) {for (String url : ipLimitUrls) {if (antPathMatcher.matchStart(url, path)) {log.warn("IpLimitFilterJwt--url={}", path);// 进行ip限流return doLimit(exchange, chain);}}}// 默认直接放行return chain.filter(exchange);}private Mono<Void> doLimit(ServerWebExchange exchange, GatewayFilterChain chain) {// 获取真实ipServerHttpRequest request = exchange.getRequest();String ip = IPUtil.getIP(request);/*** 需求:* 判断ip在20秒内请求的次数是否超过3次* 如果超过,则限制访问30秒* 等待30秒以后,才能够恢复访问*/// 正常ipString ipRedisKey = "gateway_ip:" + ip;// 被拦截的黑名单,如果存在,则表示该ip已经被限制访问String ipRedisLimitedKey = "gateway_ip:limit:" + ip;// 通过redis执行lua脚本。返回1代表限流了,返回0代表没有限流String script = "if tonumber(redis.call('ttl', KEYS[2])) > 0 then return 1 end local" +" requestTimes = redis.call('incrby', KEYS[1], 1) if tonumber(requestTimes) == 1 then" +" redis.call('expire', KEYS[1], ARGV[2]) return 0 elseif tonumber(requestTimes)" +" > tonumber(ARGV[1]) then redis.call('setex', KEYS[2], ARGV[3], ARGV[4])" +" return 1 else return 0 end";Long result = RedisUtil.Helper.execute(script, Long.class,Lists.newArrayList(ipRedisKey, ipRedisLimitedKey),continueCounts, timeInterval, limitTime, ip);if(result == 1){return renderErrorMsg(exchange, ResponseStatusEnum.SYSTEM_ERROR_BLACK_IP);}return chain.filter(exchange);}public Mono<Void> renderErrorMsg(ServerWebExchange exchange, ResponseStatusEnum statusEnum) {// 1.获得responseServerHttpResponse response = exchange.getResponse();// 2.构建jsonResultGraceJSONResult jsonResult = GraceJSONResult.exception(statusEnum);// 3.修改response的code为500response.setStatusCode(HttpStatus.INTERNAL_SERVER_ERROR);// 4.设定header类型if (!response.getHeaders().containsKey("Content-Type")) {response.getHeaders().add(HttpHeaderNames.CONTENT_TYPE.toString(), MimeTypeUtils.APPLICATION_JSON_VALUE);}// 5.转换json并且向response写入数据String jsonStr = JsonUtils.toJsonStr(jsonResult);DataBuffer dataBuffer = response.bufferFactory().wrap(jsonStr.getBytes(StandardCharsets.UTF_8));return response.writeWith(Mono.just(dataBuffer));}@Overridepublic int getOrder() {return 1;}
    }
    

注意事项

  1. 在编写lua脚本的时候最好不要一次性写完去试,因为无法进行调试,最好进行拆解。

  2. 在进行数字比较时建议加上tonumber()。如果是通过方法传参进来的一定要加,因为redisTemplate默认会把参数当做字符串传入

    image-20240809115502533

    如果不转数字就会出现上面的错误

  3. 最后也是最重要的,lua代码逻辑一定要对,否则得不到自己想要的结果需要排查很久


http://www.ppmy.cn/embedded/93420.html

相关文章

FPGA第4篇,中国FPGA芯片市场,发展分析与报告

前言 FPGA&#xff08;Field-Programmable Gate Array&#xff09;&#xff0c;即现场可编程门阵列&#xff0c;是一种可由用户配置的集成电路&#xff0c;具有高度的灵活性和强大的计算能力&#xff0c;被广泛应用于通信、航空航天、汽车电子、消费电子、数据中心、人工智能等…

MongoDB笔记1——MongoDB简介

MongoDB 什么是MongoDB 2007年10月&#xff0c;MongoDB由10gen团队所开发&#xff0c;2009年2月首度推出。 基于分布式文件存储的数据库由C语言编写。旨在为WEB应用提供可拓展的高性能数据存储解决方案。 MongoDB是一个介于关系数据库和非关系数据库&#xff08;NoSQL&…

write_sdc和write_script区别

文章目录 一、set_disable_clock_gating_check二、write_sdc和write_script区别1. write_sdc2. write_script 一、set_disable_clock_gating_check set_disable_clock_gating_check对指定的cell/pin/lib_cell/lib_pin设置是否进行clock gating的时序检查。 对于工具插入或者…

Python中各类常用内置转换函数

Python中各类常用内置转换函数 函数功能说明int(x)将 x 转换为整数类型float(x)将 x 转换为浮点数类型str(x)将 x 转换为字符串repr(x)将 x 转换为表达式字符串eval(str)计算在字符串中的有效Python表达式&#xff0c;并返回一个对象list(s)将序列 s 转换为一个列表tuple(s)将…

Linux与Docker常用运维命令一览

大家好&#xff0c;欢迎各位工友。 在博主陆陆续续的运维过程中&#xff0c;经常会用到许多运维相关的命令&#xff0c;以往都是现用现查&#xff0c;如今抽时间都记录一下&#xff0c;便于查阅和使用。 Linux常用命令 文件和目录操作 ls&#xff1a;列出目录内容cd [direc…

用Python打造精彩动画与视频,9.1 综合运用所学技术进行项目开发

第九章&#xff1a;综合项目 9.1 综合运用所学技术进行项目开发 在本章中&#xff0c;我们将综合运用前面章节所学的各种技术&#xff0c;开发一个完整的3D动画项目。这个项目将包括3D建模、动画制作、渲染&#xff0c;以及一些高级技巧&#xff0c;如光照和材质设置。通过这…

自动化报表实践小结

这一天午休刚休息完&#xff0c;财务经理就喊我&#xff1a;“***&#xff0c;我们找个会议室聊聊”。我是一脸茫然&#xff0c;心里想着&#xff0c;我跟他也没什么私下的工作交流啊&#xff0c;能聊啥呢&#xff0c;还要找个会议室&#xff1f;究竟是什么事情呢&#xff1f;有…

Stable Diffusion绘画 | 插件-prompt-all-in-one:轻松搞定提示词

插件安装包下载链接&#xff1a;sd-webui-prompt-all-in-one 安装成功后&#xff0c;正向与反向提示词输入框下方&#xff0c;会多出一排的标签。 常用功能 翻译 在下方输入框内&#xff0c;使用中文输入提示词内容&#xff1a; 点击回车↩︎后&#xff0c;会在提示词输入框…