通过reids实现
-
限流的流程图
-
在配置文件配置限流参数
1234567blackIP:
# ip 连续请求的次数
continue
-counts: ${counts:3}
# ip 判断的时间间隔,单位:秒
time-interval: ${interval:20}
# 限制的时间,单位:秒
limit-time
: ${time:30}
-
编写全局过滤器类
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126package
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实现限流的代码
1234567891011121314151617181920212223--[[
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的整合
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121package
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俱乐部!