Redis實(shí)現(xiàn)IP限流的2種方式舉例詳解
通過(guò)reids實(shí)現(xiàn)
限流的流程圖
在配置文件配置限流參數(shù)
blackIP: # ip 連續(xù)請(qǐng)求的次數(shù) continue-counts: ${counts:3} # ip 判斷的時(shí)間間隔,單位:秒 time-interval: ${interval:20} # 限制的時(shí)間,單位:秒 limit-time: ${time:30}
編寫(xiě)全局過(guò)濾器類
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)前的請(qǐng)求路徑 String path = exchange.getRequest().getURI().getPath(); // 2.獲得所有的需要限流的url List<String> ipLimitUrls = urlPathProperties.getIpLimitUrls(); // 3.校驗(yàn)并且排除excludeList if (CollUtils.isNotEmpty(ipLimitUrls)) { for (String url : ipLimitUrls) { if (antPathMatcher.matchStart(url, path)) { log.warn("IpLimitFilterJwt--url={}", path); // 進(jìn)行ip限流 return doLimit(exchange, chain); } } } // 默認(rèn)直接放行 return chain.filter(exchange); } private Mono<Void> doLimit(ServerWebExchange exchange, GatewayFilterChain chain) { // 獲取真實(shí)ip ServerHttpRequest request = exchange.getRequest(); String ip = IPUtil.getIP(request); /** * 需求: * 判斷ip在20秒內(nèi)請(qǐng)求的次數(shù)是否超過(guò)3次 * 如果超過(guò),則限制訪問(wèn)30秒 * 等待30秒以后,才能夠恢復(fù)訪問(wèn) */ // 正常ip String ipRedisKey = "gateway_ip:" + ip; // 被攔截的黑名單,如果存在,則表示該ip已經(jīng)被限制訪問(wèn) 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); // 如果訪問(wèn)次數(shù)為1,則表明是第一次訪問(wèn),在redis設(shè)置倒計(jì)時(shí) if (requestTimes == 1) { RedisUtil.KeyOps.expire(ipRedisKey, timeInterval, TimeUnit.SECONDS); } // 如果訪問(wèn)次數(shù)超過(guò)限制的次數(shù),直接將該ip存入限制的redis key,并設(shè)置限制訪問(wèn)時(shí)間 if (requestTimes > continueCounts) { // 設(shè)置該ip需要被限流的時(shí)間 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寫(xiě)入數(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; } }
通過(guò)Lua+Redis實(shí)現(xiàn)
業(yè)務(wù)流程還是和上圖差不多,只不過(guò)gateway網(wǎng)關(guān)不用再頻繁和redis進(jìn)行交互。整個(gè)限流邏輯放在redis層,通過(guò)Lua代碼嵌套
Lua實(shí)現(xiàn)限流的代碼
--[[ ipRedisLimitedKey:限流的redis key ipRedisKey:未被限流的redis key,通過(guò)此key計(jì)算訪問(wèn)次數(shù) timeInterval:訪問(wèn)時(shí)間間隔,在此時(shí)間內(nèi),訪問(wèn)到指定次數(shù)進(jìn)行限流 limitTime:限流的時(shí)長(zhǎng) ]] -- 判斷當(dāng)前ip是否已經(jīng)被限流 if redis.call("ttl", ipRedisLimitedKey) > 0 then return 1 end -- 如果沒(méi)有被限流,就讓當(dāng)前ip在redis中的值累計(jì)1 local requestTimes = redis.call("incrby", ipRedisKey, 1) -- 判斷累加后的值 if requestTimes == 1 then -- 如果累加后的值是1,說(shuō)明是第一次請(qǐng)求,設(shè)置一個(gè)時(shí)間間隔 redis.call("expire", ipRedisKey, timeInterval) return 0 elseif requestTimes > continueCounts then -- 如果累加后的值超過(guò)了設(shè)定的閾值,就對(duì)當(dāng)前ip進(jìn)行限流 redis.call("setex", ipRedisLimitedKey, limitTime, ip) return 1 end
java代碼實(shí)現(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)前的請(qǐng)求路徑 String path = exchange.getRequest().getURI().getPath(); // 2.獲得所有的需要限流的url List<String> ipLimitUrls = urlPathProperties.getIpLimitUrls(); // 3.校驗(yàn)并且排除excludeList if (CollUtils.isNotEmpty(ipLimitUrls)) { for (String url : ipLimitUrls) { if (antPathMatcher.matchStart(url, path)) { log.warn("IpLimitFilterJwt--url={}", path); // 進(jìn)行ip限流 return doLimit(exchange, chain); } } } // 默認(rèn)直接放行 return chain.filter(exchange); } private Mono<Void> doLimit(ServerWebExchange exchange, GatewayFilterChain chain) { // 獲取真實(shí)ip ServerHttpRequest request = exchange.getRequest(); String ip = IPUtil.getIP(request); /** * 需求: * 判斷ip在20秒內(nèi)請(qǐng)求的次數(shù)是否超過(guò)3次 * 如果超過(guò),則限制訪問(wèn)30秒 * 等待30秒以后,才能夠恢復(fù)訪問(wèn) */ // 正常ip String ipRedisKey = "gateway_ip:" + ip; // 被攔截的黑名單,如果存在,則表示該ip已經(jīng)被限制訪問(wèn) String ipRedisLimitedKey = "gateway_ip:limit:" + ip; // 通過(guò)redis執(zhí)行l(wèi)ua腳本。返回1代表限流了,返回0代表沒(méi)有限流 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寫(xiě)入數(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; } }
注意事項(xiàng)
在編寫(xiě)lua腳本的時(shí)候最好不要一次性寫(xiě)完去試,因?yàn)闊o(wú)法進(jìn)行調(diào)試,最好進(jìn)行拆解。
在進(jìn)行數(shù)字比較時(shí)建議加上
tonumber()
。如果是通過(guò)方法傳參進(jìn)來(lái)的一定要加,因?yàn)閞edisTemplate默認(rèn)會(huì)把參數(shù)當(dāng)做字符串傳入如果不轉(zhuǎn)數(shù)字就會(huì)出現(xiàn)上面的錯(cuò)誤
最后也是最重要的,lua代碼邏輯一定要對(duì),否則得不到自己想要的結(jié)果需要排查很久
總結(jié)
到此這篇關(guān)于Redis實(shí)現(xiàn)IP限流的2種方式的文章就介紹到這了,更多相關(guān)Redis實(shí)現(xiàn)IP限流內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Redis數(shù)據(jù)類型之散列類型hash命令學(xué)習(xí)
這篇文章主要為大家介紹了Redis數(shù)據(jù)類型之散列類型hash命令學(xué)習(xí),有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-07-07Redis?使用?List?實(shí)現(xiàn)消息隊(duì)列的優(yōu)缺點(diǎn)
這篇文章主要介紹了Redis?使用?List?實(shí)現(xiàn)消息隊(duì)列有哪些利弊,小編結(jié)合消息隊(duì)列的特點(diǎn)一步步帶大家分析使用?Redis?的?List?作為消息隊(duì)列的實(shí)現(xiàn)原理,并分享如何把?SpringBoot?與?Redission?整合運(yùn)用到項(xiàng)目中,需要的朋友可以參考下2022-01-01Redis中管道操作pipeline的實(shí)現(xiàn)
RedisPipeline是一種優(yōu)化客戶端與服務(wù)器通信的技術(shù),通過(guò)批量發(fā)送和接收命令減少網(wǎng)絡(luò)往返次數(shù),提高命令執(zhí)行效率,本文就來(lái)介紹一下Redis中管道操作pipeline的實(shí)現(xiàn),具有一定的參考價(jià)值,感興趣的可以了解一下2025-03-03Win10下通過(guò)Ubuntu安裝Redis的過(guò)程
這篇文章主要介紹了Win10下通過(guò)Ubuntu安裝Redis,在安裝Ubuntu需要先打開(kāi)Windows功能,接著創(chuàng)建一個(gè)用戶及密碼,本文給大家介紹的非常詳細(xì),需要的朋友可以參考下2022-04-04Redis開(kāi)啟鍵空間通知實(shí)現(xiàn)超時(shí)通知的步驟詳解
這篇文章主要介紹了Redis開(kāi)啟鍵空間通知實(shí)現(xiàn)超時(shí)通知的步驟,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-06-06redis鍵值出現(xiàn)\xac\xed\x00\x05t\x00&的問(wèn)題及解決
這篇文章主要介紹了redis鍵值出現(xiàn)\xac\xed\x00\x05t\x00&的問(wèn)題及解決方案,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-07-07redis適合場(chǎng)景八點(diǎn)總結(jié)
在本篇文章中我們給大家整理了關(guān)于redis適合什么場(chǎng)景的8點(diǎn)知識(shí)點(diǎn)內(nèi)容,需要的朋友們參考下。2019-06-06