本篇内容介绍了“SpringBoot+Redis+Lua怎么防止IP重复防刷攻击”的有关知识,在实际案例的操作过程中,不少人都会遇到这样的困境,接下来就让小编带领大家学习一下如何处理这些情况吧!希望大家仔细阅读,能够学有所成!
黑客或者一些恶意的用户为了攻击你的网站或者APP。通过肉机并发或者死循环请求你的接口。从而导致系统出现宕机。
针对新增数据的接口,会出现大量的重复数据,甚至垃圾数据会将你的数据库和CPU或者内存磁盘耗尽,直到数据库撑爆为止。
针对查询的接口。黑客一般是重点攻击慢查询,比如一个SQL是2S。只要黑客一致攻击,就必然造成系统被拖垮,数据库查询全都被阻塞,连接一直得不到释放造成数据库无法访问。
具体要实现和达到的效果是:
需求:在10秒内,同一IP 127.0.0.1 地址只允许访问30次。
最终达到的效果:
Long execute = this.stringRedisTemplate.execute(defaultRedisScript, keyList, "30", "10");
分析:keylist = 127.0.0.1 expire 30 incr
分析1:用户ip地址127.0.0.1 访问一次 incr
分析2:用户ip地址127.0.0.1 访问一次 incr
分析3:用户ip地址127.0.0.1 访问一次 incr
分析4:用户ip地址127.0.0.1 访问一次 incr
分析10:用户ip地址127.0.0.1 访问一次 incr
判断当前的次数是否以及达到了10次,如果达到了。就时间当前时间是否已经大于30秒。如果没有大于就不允许访问,否则开始设置过期
方法一:根据用户id或者ip来实现
lua文件
在resource/lua下面创建iplimit.lua文件
-- 为某个接口的请求IP设置计数器,比如:127.0.0.1请求课程接口-- KEYS[1] = 127.0.0.1 也就是用户的IP-- ARGV[1] = 过期时间 30m-- ARGV[2] = 限制的次数local limitCount = redis.call('incr',KEYS[1]);if limitCount == 1 then redis.call("expire",KEYS[1],ARGV[1]) end-- 如果次数还没有过期,并且还在规定的次数内,说明还在请求同一接口if limitCount > tonumber(ARGV[2]) then return 0endreturn 1
创建lua对象
@SpringBootConfigurationpublic class LuaConfiguration { @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;@RestControllerpublic 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对应的值,是一个集合 List<String> keysList = new ArrayList<>(); keysList.add(key); // 2:具体的值ARGV 他是一个动态参数,起也就是一个数组 // 10 代表过期时间 2次数,表述:10秒之内最多允许2次访问 Long execute = stringRedisTemplate.execute(iplimitLua, keysList,"10","2"); if (execute == 0) { log.info("1----->ip:{},请求收到限制", key); return "客官,不要太快了服务反应不过来..."; } log.info("2----->ip:{},正常访问,返回课程列表", key); return "正常访问,返回课程列表 " + key; }}
其实还可以自己写一个自定义的注解,结合lua来实现限流
比如:@iplimit(time=10,limit=2)
#方法二:注解实现
需求:用户请求在一秒钟之内只允许2个请求。
核心是AOP
前面几步是一样的,lua脚本,lua对象,自定义redisltemplate。
redis依赖
<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>
lua脚本
-- 为某个接口的请求IP设置计数器,比如:127.0.0.1请求课程接口-- KEYS[1] = 127.0.0.1 也就是用户的IP-- ARGV[1] = 过期时间 30m-- ARGV[2] = 限制的次数local limitCount = redis.call('incr',KEYS[1]);if limitCount == 1 then redis.call("expire",KEYS[1],ARGV[1])end-- 如果次数还没有过期,并且还在规定的次数内,说明还在请求同一接口if limitCount > tonumber(ARGV[2]) then return 0endreturn 1
创建lua对象
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;@Configurationpublic class LuaConfiguration { @Bean public DefaultRedisScript<Boolean> limitUserAccessLua() { // 1: 初始化一个lua脚本的对象DefaultRedisScript DefaultRedisScript<Boolean> defaultRedisScript = new DefaultRedisScript<>(); // 2: 通过这个对象去加载lua脚本的位置 ClassPathResource读取类路径下的lua脚本 // ClassPathResource 什么是类路径:就是你maven编译好的target/classes目录 defaultRedisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("lua/userlimit.lua"))); // 3: lua脚本最终的返回值是什么?建议大家都是数字返回。1/0 defaultRedisScript.setResultType(Boolean.class); return defaultRedisScript; }}
自定义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;@Configurationpublic class RedisConfiguration { @Bean public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) { // 1: 开始创建一个redistemplate RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>(); // 2:开始redis连接工厂跪安了 redisTemplate.setConnectionFactory(redisConnectionFactory); // 创建一个json的序列化方式 GenericJackson2JsonRedisSerializer jackson2JsonRedisSerializer = new GenericJackson2JsonRedisSerializer(); // 设置key用string序列化方式 redisTemplate.setKeySerializer(new StringRedisSerializer()); // 设置value用jackjson进行处理 redisTemplate.setValueSerializer(jackson2JsonRedisSerializer); // hash也要进行修改 redisTemplate.setHashKeySerializer(new StringRedisSerializer()); redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer); // 默认调用 redisTemplate.afterPropertiesSet(); return redisTemplate; }}
自定义注解
package com.kuangstudy.limit.annotation;import java.lang.annotation.*;@Target(ElementType.METHOD)@Retention(RetentionPolicy.RUNTIME)@Documentedpublic @interface AccessLimiter { // 目标: @AccessLimiter(limit="1",timeout="1",key="user:ip:limit") // 解读:一个用户key在timeout时间内,最多访问limit次 // 缓存的key String key(); // 限制的次数 int limit() default 1; // 过期时间 int timeout() default 1;}
自定义切面
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@Componentpublic class AccessLimiterAspect { private static final Logger log = LoggerFactory.getLogger(AccessLimiterAspect.class); @Autowired private StringRedisTemplate stringRedisTemplate; @Autowired private DefaultRedisScript<Boolean> limitUserAccessLua; // 1: 切入点 @Pointcut("@annotation(com.kuangstudy.limit.annotation.AccessLimiter)") public void cut() { System.out.println("cut"); } // 2: 通知和连接点 @Before("cut()") public void before(JoinPoint joinPoint) { // 1: 获取到执行的方法 MethodSignature signature = (MethodSignature) joinPoint.getSignature(); Method method = signature.getMethod(); // 2:通过方法获取到注解 AccessLimiter annotation = method.getAnnotation(AccessLimiter.class); // 如果 annotation==null,说明方法上没加限流AccessLimiter,说明不需要限流操作 if (annotation == null) { return; } // 3: 获取到对应的注解参数 String key = annotation.key(); Integer limit = annotation.limit(); Integer timeout = annotation.timeout(); // 4: 如果你的key是空的 if (StringUtils.isEmpty(key)) { String name = method.getDeclaringClass().getName(); // 直接把当前的方法名给与key key = name+"#"+method.getName(); // 获取方法中的参数列表 //ParameterNameDiscoverer pnd = new DefaultParameterNameDiscoverer(); //String[] parameterNames = pnd.getParameterNames(method); Class<?>[] parameterTypes = method.getParameterTypes(); for (Class<?> parameterType : parameterTypes) { System.out.println(parameterType); } // 如果方法有参数,那么就把key规则 = 方法名“#”参数类型 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:执行执行lua脚本限流 Boolean accessFlag = stringRedisTemplate.execute(limitUserAccessLua, keysList, limit.toString(), timeout.toString()); // 3: 判断当前执行的结果,如果是0,被限制,1代表正常 if (!accessFlag) { throw new BusinessException(500, "server is busy!!!"); } }}
在需要限流的方法上进行限流测试
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;@RestControllerpublic class RateLimiterController { @Autowired private StringRedisTemplate stringRedisTemplate; @Autowired private DefaultRedisScript<Boolean> limitUserAccessLua; @GetMapping("/limit/user") public String limitUser(String userid) { // 1: 定义key是的列表 List<String> keysList = new ArrayList<>(); keysList.add("user:"+userid); // 2:执行执行lua脚本限流 Boolean accessFlag = stringRedisTemplate.execute(limitUserAccessLua, keysList, "1","1"); // 3: 判断当前执行的结果,如果是0,被限制,1代表正常 if (!accessFlag) { throw new BusinessException(500,"server is busy!!!"); } return "scucess"; } @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"; } @GetMapping("/limit/aop/user2") public String limitAopUser2(String userid) { return "scucess"; }}
“SpringBoot+Redis+Lua怎么防止IP重复防刷攻击”的内容就介绍到这里了,感谢大家的阅读。如果想了解更多行业相关的知识可以关注编程网网站,小编将为大家输出更多高质量的实用文章!