Redis實現(xiàn)IP限流的2種方式舉例詳解
通過reids實現(xiàn)
限流的流程圖
在配置文件配置限流參數(shù)
blackIP: # ip 連續(xù)請求的次數(shù) 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.獲取當(dāng)前的請求路徑 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秒內(nèi)請求的次數(shù)是否超過3次 * 如果超過,則限制訪問30秒 * 等待30秒以后,才能夠恢復(fù)訪問 */ // 正常ip String ipRedisKey = "gateway_ip:" + ip; // 被攔截的黑名單,如果存在,則表示該ip已經(jīng)被限制訪問 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的累加次數(shù) long requestTimes = RedisUtil.StringOps.incrBy(ipRedisKey, 1); // 如果訪問次數(shù)為1,則表明是第一次訪問,在redis設(shè)置倒計時 if (requestTimes == 1) { RedisUtil.KeyOps.expire(ipRedisKey, timeInterval, TimeUnit.SECONDS); } // 如果訪問次數(shù)超過限制的次數(shù),直接將該ip存入限制的redis key,并設(shè)置限制訪問時間 if (requestTimes > continueCounts) { // 設(shè)置該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.構(gòu)建jsonResult GraceJSONResult jsonResult = GraceJSONResult.exception(statusEnum); // 3.修改response的code為500 response.setStatusCode(HttpStatus.INTERNAL_SERVER_ERROR); // 4.設(shè)定header類型 if (!response.getHeaders().containsKey("Content-Type")) { response.getHeaders().add(HttpHeaderNames.CONTENT_TYPE.toString(), MimeTypeUtils.APPLICATION_JSON_VALUE); } // 5.轉(zhuǎn)換json并且向response寫入數(shù)據(jù) 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實現(xiàn)
業(yè)務(wù)流程還是和上圖差不多,只不過gateway網(wǎng)關(guān)不用再頻繁和redis進行交互。整個限流邏輯放在redis層,通過Lua代碼嵌套
Lua實現(xiàn)限流的代碼
--[[ ipRedisLimitedKey:限流的redis key ipRedisKey:未被限流的redis key,通過此key計算訪問次數(shù) timeInterval:訪問時間間隔,在此時間內(nèi),訪問到指定次數(shù)進行限流 limitTime:限流的時長 ]] -- 判斷當(dāng)前ip是否已經(jīng)被限流 if redis.call("ttl", ipRedisLimitedKey) > 0 then return 1 end -- 如果沒有被限流,就讓當(dāng)前ip在redis中的值累計1 local requestTimes = redis.call("incrby", ipRedisKey, 1) -- 判斷累加后的值 if requestTimes == 1 then -- 如果累加后的值是1,說明是第一次請求,設(shè)置一個時間間隔 redis.call("expire", ipRedisKey, timeInterval) return 0 elseif requestTimes > continueCounts then -- 如果累加后的值超過了設(shè)定的閾值,就對當(dāng)前ip進行限流 redis.call("setex", ipRedisLimitedKey, limitTime, ip) return 1 end
java代碼實現(xiàn)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.獲取當(dāng)前的請求路徑 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秒內(nèi)請求的次數(shù)是否超過3次 * 如果超過,則限制訪問30秒 * 等待30秒以后,才能夠恢復(fù)訪問 */ // 正常ip String ipRedisKey = "gateway_ip:" + ip; // 被攔截的黑名單,如果存在,則表示該ip已經(jīng)被限制訪問 String ipRedisLimitedKey = "gateway_ip:limit:" + ip; // 通過redis執(zhí)行l(wèi)ua腳本。返回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.構(gòu)建jsonResult GraceJSONResult jsonResult = GraceJSONResult.exception(statusEnum); // 3.修改response的code為500 response.setStatusCode(HttpStatus.INTERNAL_SERVER_ERROR); // 4.設(shè)定header類型 if (!response.getHeaders().containsKey("Content-Type")) { response.getHeaders().add(HttpHeaderNames.CONTENT_TYPE.toString(), MimeTypeUtils.APPLICATION_JSON_VALUE); } // 5.轉(zhuǎn)換json并且向response寫入數(shù)據(jù) 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腳本的時候最好不要一次性寫完去試,因為無法進行調(diào)試,最好進行拆解。
在進行數(shù)字比較時建議加上
tonumber()
。如果是通過方法傳參進來的一定要加,因為redisTemplate默認會把參數(shù)當(dāng)做字符串傳入如果不轉(zhuǎn)數(shù)字就會出現(xiàn)上面的錯誤
最后也是最重要的,lua代碼邏輯一定要對,否則得不到自己想要的結(jié)果需要排查很久
總結(jié)
到此這篇關(guān)于Redis實現(xiàn)IP限流的2種方式的文章就介紹到這了,更多相關(guān)Redis實現(xiàn)IP限流內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Redis數(shù)據(jù)類型之散列類型hash命令學(xué)習(xí)
這篇文章主要為大家介紹了Redis數(shù)據(jù)類型之散列類型hash命令學(xué)習(xí),有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2023-07-07Redis?使用?List?實現(xiàn)消息隊列的優(yōu)缺點
這篇文章主要介紹了Redis?使用?List?實現(xiàn)消息隊列有哪些利弊,小編結(jié)合消息隊列的特點一步步帶大家分析使用?Redis?的?List?作為消息隊列的實現(xiàn)原理,并分享如何把?SpringBoot?與?Redission?整合運用到項目中,需要的朋友可以參考下2022-01-01redis鍵值出現(xiàn)\xac\xed\x00\x05t\x00&的問題及解決
這篇文章主要介紹了redis鍵值出現(xiàn)\xac\xed\x00\x05t\x00&的問題及解決方案,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2023-07-07