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<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代码嵌套
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<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; } }
注意事项
在编写lua脚本的时候最好不要一次性写完去试,因为无法进行调试,最好进行拆解。
在进行数字比较时建议加上
tonumber()
。如果是通过方法传参进来的一定要加,因为redisTemplate默认会把参数当做字符串传入如果不转数字就会出现上面的错误
最后也是最重要的,lua代码逻辑一定要对,否则得不到自己想要的结果需要排查很久
总结
到此这篇关于Redis实现IP限流的2种方式的文章就介绍到这了,更多相关Redis实现IP限流内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!