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

gateway网关ip限流

通过reids实现

  1. 限流的流程图

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

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

    java 复制代码
    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 {
    
        @Autowired
        private 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();
    
        @Override
        public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
            // 1.获取当前的请求路径
            String path = exchange.getRequest().getURI().getPath();
    
            // 2.获得所有的需要限流的url
            List<String> ipLimitUrls = urlPathProperties.getIpLimitUrls();
            // 3.校验并且排除excludeList
            if (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) {
            // 获取真实ip
            ServerHttpRequest request = exchange.getRequest();
            String ip = IPUtil.getIP(request);
    
            /**
             * 需求:
             * 判断ip在20秒内请求的次数是否超过3次
             * 如果超过,则限制访问30秒
             * 等待30秒以后,才能够恢复访问
             */
            // 正常ip
            String 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.获得response
            ServerHttpResponse response = exchange.getResponse();
            // 2.构建jsonResult
            GraceJSONResult jsonResult = GraceJSONResult.exception(statusEnum);
            // 3.修改response的code为500
            response.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));
        }
    
        @Override
        public 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 then
        return 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的整合

    java 复制代码
    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 {
    
        @Autowired
        private 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();
    
        @Override
        public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
            // 1.获取当前的请求路径
            String path = exchange.getRequest().getURI().getPath();
    
            // 2.获得所有的需要限流的url
            List<String> ipLimitUrls = urlPathProperties.getIpLimitUrls();
            // 3.校验并且排除excludeList
            if (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) {
            // 获取真实ip
            ServerHttpRequest request = exchange.getRequest();
            String ip = IPUtil.getIP(request);
    
            /**
             * 需求:
             * 判断ip在20秒内请求的次数是否超过3次
             * 如果超过,则限制访问30秒
             * 等待30秒以后,才能够恢复访问
             */
            // 正常ip
            String 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.获得response
            ServerHttpResponse response = exchange.getResponse();
            // 2.构建jsonResult
            GraceJSONResult jsonResult = GraceJSONResult.exception(statusEnum);
            // 3.修改response的code为500
            response.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));
        }
    
        @Override
        public int getOrder() {
            return 1;
        }
    }

注意事项

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

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

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

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

相关推荐
掘金-我是哪吒1 小时前
微服务mysql,redis,elasticsearch, kibana,cassandra,mongodb, kafka
redis·mysql·mongodb·elasticsearch·微服务
ketil273 小时前
Ubuntu 安装 redis
redis
王佑辉4 小时前
【redis】redis缓存和数据库保证一致性的方案
redis·面试
Karoku0665 小时前
【企业级分布式系统】Zabbix监控系统与部署安装
运维·服务器·数据库·redis·mysql·zabbix
gorgor在码农5 小时前
Redis 热key总结
java·redis·热key
想进大厂的小王5 小时前
项目架构介绍以及Spring cloud、redis、mq 等组件的基本认识
redis·分布式·后端·spring cloud·微服务·架构
Java 第一深情5 小时前
高性能分布式缓存Redis-数据管理与性能提升之道
redis·分布式·缓存
minihuabei10 小时前
linux centos 安装redis
linux·redis·centos
monkey_meng12 小时前
【Rust中多线程同步机制】
开发语言·redis·后端·rust
hlsd#13 小时前
go 集成go-redis 缓存操作
redis·缓存·golang