IT俱乐部 Redis Redis实现IP限流的2种方式举例详解

Redis实现IP限流的2种方式举例详解

通过reids实现

  • 限流的流程图

  • 在配置文件配置限流参数

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

    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 filter(ServerWebExchange exchange, GatewayFilterChain chain) {
            // 1.获取当前的请求路径
            String path = exchange.getRequest().getURI().getPath();
    
            // 2.获得所有的需要限流的url
            List 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 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 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代码嵌套

  • 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
    
  • 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 {
    
        @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 filter(ServerWebExchange exchange, GatewayFilterChain chain) {
            // 1.获取当前的请求路径
            String path = exchange.getRequest().getURI().getPath();
    
            // 2.获得所有的需要限流的url
            List 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 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 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脚本的时候最好不要一次性写完去试,因为无法进行调试,最好进行拆解。

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

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

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

总结 

到此这篇关于Redis实现IP限流的2种方式的文章就介绍到这了,更多相关Redis实现IP限流内容请搜索IT俱乐部以前的文章或继续浏览下面的相关文章希望大家以后多多支持IT俱乐部!

本文收集自网络,不代表IT俱乐部立场,转载请注明出处。https://www.2it.club/database/redis/12924.html
上一篇
下一篇
联系我们

联系我们

在线咨询: QQ交谈

邮箱: 1120393934@qq.com

工作时间:周一至周五,9:00-17:30,节假日休息

关注微信
微信扫一扫关注我们

微信扫一扫关注我们

返回顶部