gateway网关ip限流
通过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 {@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代码嵌套
-
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
-
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;} }