SpringBoot+Redis+Lua防止IP重復(fù)防刷攻擊的方法
黑客或者一些惡意的用戶(hù)為了攻擊你的網(wǎng)站或者APP。通過(guò)肉機(jī)并發(fā)或者死循環(huán)請(qǐng)求你的接口。從而導(dǎo)致系統(tǒng)出現(xiàn)宕機(jī)。
- 針對(duì)新增數(shù)據(jù)的接口,會(huì)出現(xiàn)大量的重復(fù)數(shù)據(jù),甚至垃圾數(shù)據(jù)會(huì)將你的數(shù)據(jù)庫(kù)和CPU或者內(nèi)存磁盤(pán)耗盡,直到數(shù)據(jù)庫(kù)撐爆為止。
- 針對(duì)查詢(xún)的接口。黑客一般是重點(diǎn)攻擊慢查詢(xún),比如一個(gè)SQL是2S。只要黑客一致攻擊,就必然造成系統(tǒng)被拖垮,數(shù)據(jù)庫(kù)查詢(xún)?nèi)急蛔枞?,連接一直得不到釋放造成數(shù)據(jù)庫(kù)無(wú)法訪問(wèn)。
具體要實(shí)現(xiàn)和達(dá)到的效果是:
需求:在10秒內(nèi),同一IP 127.0.0.1 地址只允許訪問(wèn)30次。
最終達(dá)到的效果:
Long execute = this.stringRedisTemplate.execute(defaultRedisScript, keyList, "30", "10");
分析:keylist = 127.0.0.1 expire 30 incr
- 分析1:用戶(hù)ip地址127.0.0.1 訪問(wèn)一次 incr
- 分析2:用戶(hù)ip地址127.0.0.1 訪問(wèn)一次 incr
- 分析3:用戶(hù)ip地址127.0.0.1 訪問(wèn)一次 incr
- 分析4:用戶(hù)ip地址127.0.0.1 訪問(wèn)一次 incr
- 分析10:用戶(hù)ip地址127.0.0.1 訪問(wèn)一次 incr
- 判斷當(dāng)前的次數(shù)是否以及達(dá)到了10次,如果達(dá)到了。就時(shí)間當(dāng)前時(shí)間是否已經(jīng)大于30秒。如果沒(méi)有大于就不允許訪問(wèn),否則開(kāi)始設(shè)置過(guò)期
方法一:根據(jù)用戶(hù)id或者ip來(lái)實(shí)現(xiàn)
第一步:lua文件
在resource/lua下面創(chuàng)建iplimit.lua文件
-- 為某個(gè)接口的請(qǐng)求IP設(shè)置計(jì)數(shù)器,比如:127.0.0.1請(qǐng)求課程接口
-- KEYS[1] = 127.0.0.1 也就是用戶(hù)的IP
-- ARGV[1] = 過(guò)期時(shí)間 30m
-- ARGV[2] = 限制的次數(shù)
local limitCount = redis.call('incr',KEYS[1]);
if limitCount == 1 then
redis.call("expire",KEYS[1],ARGV[1])
end
-- 如果次數(shù)還沒(méi)有過(guò)期,并且還在規(guī)定的次數(shù)內(nèi),說(shuō)明還在請(qǐng)求同一接口
if limitCount > tonumber(ARGV[2]) then
return 0
end
return 1
第二步:創(chuàng)建lua對(duì)象
@SpringBootConfiguration
public class LuaConfiguration {
/**
* 將lua腳本的內(nèi)容加載出來(lái)放入到DefaultRedisScript
* @return
*/
@Bean
public DefaultRedisScript<Long> initluascript() {
DefaultRedisScript<Long> defaultRedisScript = new DefaultRedisScript<>();
defaultRedisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("lua/iplimit.lua")));
defaultRedisScript.setResultType(Long.class);
return defaultRedisScript;
}
}
第三步使用
package com.kuangstudy.controller;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.ArrayList;
import java.util.List;
/**
* @description:
* @author: xuke
* @time: 2021/7/3 22:25
*/
@RestController
public class IpLuaController {
private static final Logger log = LoggerFactory.getLogger(IpLuaController.class);
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Autowired
private DefaultRedisScript<Long> iplimitLua;
@PostMapping("/ip/limit")
//@IpList(second=10,limit=20)
public String luaupdateuser(String ip) {
String key = "user:" + ip;
// 1: KEYS對(duì)應(yīng)的值,是一個(gè)集合
List<String> keysList = new ArrayList<>();
keysList.add(key);
// 2:具體的值A(chǔ)RGV 他是一個(gè)動(dòng)態(tài)參數(shù),起也就是一個(gè)數(shù)組
// 10 代表過(guò)期時(shí)間 2次數(shù),表述:10秒之內(nèi)最多允許2次訪問(wèn)
Long execute = stringRedisTemplate.execute(iplimitLua, keysList,"10","2");
if (execute == 0) {
log.info("1----->ip:{},請(qǐng)求收到限制", key);
return "客官,不要太快了服務(wù)反應(yīng)不過(guò)來(lái)...";
}
log.info("2----->ip:{},正常訪問(wèn),返回課程列表", key);
return "正常訪問(wèn),返回課程列表 " + key;
}
}
其實(shí)還可以自己寫(xiě)一個(gè)自定義的注解,結(jié)合lua來(lái)實(shí)現(xiàn)限流
比如:@iplimit(time=10,limit=2)
#方法二:注解實(shí)現(xiàn)
需求:用戶(hù)請(qǐng)求在一秒鐘之內(nèi)只允許2個(gè)請(qǐng)求。
核心是AOP
前面幾步是一樣的,lua腳本,lua對(duì)象,自定義redisltemplate。
1.redis依賴(lài)
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!--這里就是redis的核心jar包-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
2.lua腳本
-- 為某個(gè)接口的請(qǐng)求IP設(shè)置計(jì)數(shù)器,比如:127.0.0.1請(qǐng)求課程接口
-- KEYS[1] = 127.0.0.1 也就是用戶(hù)的IP
-- ARGV[1] = 過(guò)期時(shí)間 30m
-- ARGV[2] = 限制的次數(shù)
local limitCount = redis.call('incr',KEYS[1]);
if limitCount == 1 then
redis.call("expire",KEYS[1],ARGV[1])
end
-- 如果次數(shù)還沒(méi)有過(guò)期,并且還在規(guī)定的次數(shù)內(nèi),說(shuō)明還在請(qǐng)求同一接口
if limitCount > tonumber(ARGV[2]) then
return 0
end
return 1
3.創(chuàng)建lua對(duì)象
package com.kuangstudy.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.scripting.support.ResourceScriptSource;
/**
* @author 飛哥
* @Title: 學(xué)相伴出品
* @Description: 我們有一個(gè)學(xué)習(xí)網(wǎng)站:https://www.kuangstudy.com
* @date 2021/5/21 12:01
*/
@Configuration
public class LuaConfiguration {
/**
* 將lua腳本的內(nèi)容加載出來(lái)放入到DefaultRedisScript
* @return
*/
@Bean
public DefaultRedisScript<Boolean> limitUserAccessLua() {
// 1: 初始化一個(gè)lua腳本的對(duì)象DefaultRedisScript
DefaultRedisScript<Boolean> defaultRedisScript = new DefaultRedisScript<>();
// 2: 通過(guò)這個(gè)對(duì)象去加載lua腳本的位置 ClassPathResource讀取類(lèi)路徑下的lua腳本
// ClassPathResource 什么是類(lèi)路徑:就是你maven編譯好的target/classes目錄
defaultRedisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("lua/userlimit.lua")));
// 3: lua腳本最終的返回值是什么?建議大家都是數(shù)字返回。1/0
defaultRedisScript.setResultType(Boolean.class);
return defaultRedisScript;
}
}
4.自定義redistemplate
package com.kuangstudy.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
/**
* @author 飛哥
* @Title: 學(xué)相伴出品
* @Description: 我們有一個(gè)學(xué)習(xí)網(wǎng)站:https://www.kuangstudy.com
* @date 2021/5/20 13:16
*/
@Configuration
public class RedisConfiguration {
/**
* @return org.springframework.data.redis.core.RedisTemplate<java.lang.String, java.lang.Object>
* @Description 改寫(xiě)redistemplate序列化規(guī)則
**/
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
// 1: 開(kāi)始創(chuàng)建一個(gè)redistemplate
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
// 2:開(kāi)始redis連接工廠跪安了
redisTemplate.setConnectionFactory(redisConnectionFactory);
// 創(chuàng)建一個(gè)json的序列化方式
GenericJackson2JsonRedisSerializer jackson2JsonRedisSerializer = new GenericJackson2JsonRedisSerializer();
// 設(shè)置key用string序列化方式
redisTemplate.setKeySerializer(new StringRedisSerializer());
// 設(shè)置value用jackjson進(jìn)行處理
redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
// hash也要進(jìn)行修改
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);
// 默認(rèn)調(diào)用
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
}
5.自定義注解
package com.kuangstudy.limit.annotation;
import java.lang.annotation.*;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface AccessLimiter {
// 目標(biāo): @AccessLimiter(limit="1",timeout="1",key="user:ip:limit")
// 解讀:一個(gè)用戶(hù)key在timeout時(shí)間內(nèi),最多訪問(wèn)limit次
// 緩存的key
String key();
// 限制的次數(shù)
int limit() default 1;
// 過(guò)期時(shí)間
int timeout() default 1;
}
6.自定義切面
package com.kuangstudy.limit.aop;
import com.kuangstudy.common.exception.BusinessException;
import com.kuangstudy.limit.annotation.AccessLimiter;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
@Aspect
@Component
public class AccessLimiterAspect {
private static final Logger log = LoggerFactory.getLogger(AccessLimiterAspect.class);
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Autowired
private DefaultRedisScript<Boolean> limitUserAccessLua;
// 1: 切入點(diǎn)
@Pointcut("@annotation(com.kuangstudy.limit.annotation.AccessLimiter)")
public void cut() {
System.out.println("cut");
}
// 2: 通知和連接點(diǎn)
@Before("cut()")
public void before(JoinPoint joinPoint) {
// 1: 獲取到執(zhí)行的方法
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
// 2:通過(guò)方法獲取到注解
AccessLimiter annotation = method.getAnnotation(AccessLimiter.class);
// 如果 annotation==null,說(shuō)明方法上沒(méi)加限流AccessLimiter,說(shuō)明不需要限流操作
if (annotation == null) {
return;
}
// 3: 獲取到對(duì)應(yīng)的注解參數(shù)
String key = annotation.key();
Integer limit = annotation.limit();
Integer timeout = annotation.timeout();
// 4: 如果你的key是空的
if (StringUtils.isEmpty(key)) {
String name = method.getDeclaringClass().getName();
// 直接把當(dāng)前的方法名給與key
key = name+"#"+method.getName();
// 獲取方法中的參數(shù)列表
//ParameterNameDiscoverer pnd = new DefaultParameterNameDiscoverer();
//String[] parameterNames = pnd.getParameterNames(method);
Class<?>[] parameterTypes = method.getParameterTypes();
for (Class<?> parameterType : parameterTypes) {
System.out.println(parameterType);
}
// 如果方法有參數(shù),那么就把key規(guī)則 = 方法名“#”參數(shù)類(lèi)型
if (parameterTypes != null) {
String paramtypes = Arrays.stream(parameterTypes)
.map(Class::getName)
.collect(Collectors.joining(","));
key = key +"#" + paramtypes;
}
}
// 1: 定義key是的列表
List<String> keysList = new ArrayList<>();
keysList.add(key);
// 2:執(zhí)行執(zhí)行l(wèi)ua腳本限流
Boolean accessFlag = stringRedisTemplate.execute(limitUserAccessLua, keysList, limit.toString(), timeout.toString());
// 3: 判斷當(dāng)前執(zhí)行的結(jié)果,如果是0,被限制,1代表正常
if (!accessFlag) {
throw new BusinessException(500, "server is busy!!!");
}
}
}
7.在需要限流的方法上進(jìn)行限流測(cè)試
package com.kuangstudy.controller;
import com.kuangstudy.common.exception.BusinessException;
import com.kuangstudy.limit.annotation.AccessLimiter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.ArrayList;
import java.util.List;
@RestController
public class RateLimiterController {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Autowired
private DefaultRedisScript<Boolean> limitUserAccessLua;
/**
* 限流的處理方法
* @param userid
* @return
*/
@GetMapping("/limit/user")
public String limitUser(String userid) {
// 1: 定義key是的列表
List<String> keysList = new ArrayList<>();
keysList.add("user:"+userid);
// 2:執(zhí)行執(zhí)行l(wèi)ua腳本限流
Boolean accessFlag = stringRedisTemplate.execute(limitUserAccessLua, keysList, "1","1");
// 3: 判斷當(dāng)前執(zhí)行的結(jié)果,如果是0,被限制,1代表正常
if (!accessFlag) {
throw new BusinessException(500,"server is busy!!!");
}
return "scucess";
}
/**
* 限流的處理方法
* @param userid
* @return
*
* 方案1:如果你的一個(gè)方法進(jìn)行限流:一個(gè)方法只允許1秒100請(qǐng)求,key公用
* 方案2:如果你的一個(gè)方法進(jìn)行限流:某個(gè)用戶(hù)一秒之內(nèi)允許10個(gè)請(qǐng)求,key必須要根據(jù)參數(shù)的具體值去執(zhí)行拼接。
*
*/
@GetMapping("/limit/aop/user")
@AccessLimiter(limit = 1,timeout = 1)
public String limitAopUser(String userid) {
return "scucess";
}
@GetMapping("/limit/aop/user3")
@AccessLimiter(limit = 10,timeout = 1)
public String limitAopUse3(String userid) {
return "scucess";
}
/**
* 限流的處理方法
* @param userid
* @return
*
* 方案1:如果你的一個(gè)方法進(jìn)行限流:一個(gè)方法只允許1秒100請(qǐng)求,key公用
* 方案2:如果你的一個(gè)方法進(jìn)行限流:某個(gè)用戶(hù)一秒之內(nèi)允許10個(gè)請(qǐng)求,key必須要根據(jù)參數(shù)的具體值去執(zhí)行拼接。
*
*/
@GetMapping("/limit/aop/user2")
public String limitAopUser2(String userid) {
return "scucess";
}
}
到此這篇關(guān)于SpringBoot+Redis+Lua防止IP重復(fù)防刷攻擊的方法的文章就介紹到這了,更多相關(guān)SpringBoot防止IP重復(fù)防刷 內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- SpringBoot利用Redis實(shí)現(xiàn)防止訂單重復(fù)提交的解決方案
- SpringBoot中防止接口重復(fù)提交的有效方法
- SpringBoot攔截器實(shí)現(xiàn)項(xiàng)目防止接口重復(fù)提交
- SpringBoot整合redis+Aop防止重復(fù)提交的實(shí)現(xiàn)
- SpringBoot+Redis使用AOP防止重復(fù)提交的實(shí)現(xiàn)
- SpringBoot?使用AOP?+?Redis?防止表單重復(fù)提交的方法
- springboot 防止重復(fù)請(qǐng)求防止重復(fù)點(diǎn)擊的操作
- SpringBoot整合ShedLock解決定時(shí)任務(wù)防止重復(fù)執(zhí)行的問(wèn)題
相關(guān)文章
關(guān)于Java的HashMap多線程并發(fā)問(wèn)題分析
HashMap是采用鏈表解決Hash沖突,因?yàn)槭擎湵斫Y(jié)構(gòu),那么就很容易形成閉合的鏈路,這樣在循環(huán)的時(shí)候只要有線程對(duì)這個(gè)HashMap進(jìn)行g(shù)et操作就會(huì)產(chǎn)生死循環(huán),本文針對(duì)這個(gè)問(wèn)題進(jìn)行分析,需要的朋友可以參考下2023-05-05
idea推送項(xiàng)目到gitee中的創(chuàng)建方法
這篇文章主要介紹了idea推送項(xiàng)目到gitee中的創(chuàng)建方法,本文通過(guò)圖文實(shí)例相結(jié)合給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2021-08-08
IntelliJ IDEA語(yǔ)法報(bào)錯(cuò)"Usage of API documented as @since 1.6+"的解決
今天小編就為大家分享一篇關(guān)于IntelliJ IDEA語(yǔ)法報(bào)錯(cuò)"Usage of API documented as @since 1.6+"的解決辦法,小編覺(jué)得內(nèi)容挺不錯(cuò)的,現(xiàn)在分享給大家,具有很好的參考價(jià)值,需要的朋友一起跟隨小編來(lái)看看吧2018-10-10

