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

3 篇文章 0 订阅
2 篇文章 0 订阅

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 {
    
        @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实现限流的代码

    --[[
    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的整合

    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默认会把参数当做字符串传入

    image-20240809115502533

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

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

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值