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 thenredis.call("expire",KEYS[1],ARGV[1]) end-- 如果次数还没有过期,并且还在规定的次数内,说明还在请求同一接口if limitCount > tonumber(ARGV[2]) thenreturn 0endreturn 1

第二步:创建lua对象

@SpringBootConfigurationpublic class LuaConfiguration {/*** 将lua脚本的内容加载出来放入到DefaultRedisScript* @return*/@Beanpublic DefaultRedisScript initluascript() {DefaultRedisScript 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 */@RestControllerpublic class IpLuaController {private static final Logger log = LoggerFactory.getLogger(IpLuaController.class); @Autowiredprivate StringRedisTemplate stringRedisTemplate; @Autowiredprivate DefaultRedisScript iplimitLua; @PostMapping("/ip/limit")//@IpList(second=10,limit=20)public String luaupdateuser(String ip) {String key = "user:" + ip; // 1: KEYS对应的值,是一个集合List 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。
1.redis依赖

org.springframework.bootspring-boot-starter-aoporg.springframework.bootspring-boot-starter-weborg.springframework.bootspring-boot-starter-testtestorg.springframework.bootspring-boot-starter-data-redis

2.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 thenredis.call("expire",KEYS[1],ARGV[1])end-- 如果次数还没有过期,并且还在规定的次数内,说明还在请求同一接口if limitCount > tonumber(ARGV[2]) thenreturn 0endreturn 1

【SpringBoot+Redis+Lua防止IP重复防刷攻击的方法】3.创建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; /** * @author 飞哥 * @Title: 学相伴出品 * @Description: 我们有一个学习网站:https://www.kuangstudy.com * @date 2021/5/21 12:01 */@Configurationpublic class LuaConfiguration {/*** 将lua脚本的内容加载出来放入到DefaultRedisScript* @return*/@Beanpublic DefaultRedisScript limitUserAccessLua() {// 1: 初始化一个lua脚本的对象DefaultRedisScriptDefaultRedisScript defaultRedisScript = new DefaultRedisScript<>(); // 2: 通过这个对象去加载lua脚本的位置 ClassPathResource读取类路径下的lua脚本// ClassPathResource 什么是类路径:就是你maven编译好的target/classes目录defaultRedisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("lua/userlimit.lua"))); // 3: lua脚本最终的返回值是什么?建议大家都是数字返回。1/0defaultRedisScript.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: 学相伴出品 * @Description: 我们有一个学习网站:https://www.kuangstudy.com * @date 2021/5/20 13:16 */@Configurationpublic class RedisConfiguration {/*** @return org.springframework.data.redis.core.RedisTemplate* @Description 改写redistemplate序列化规则**/@Beanpublic RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory) {// 1: 开始创建一个redistemplateRedisTemplate 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; }}

5.自定义注解

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次// 缓存的keyString key(); // 限制的次数int limit() default1; // 过期时间int timeout() default1; }

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@Componentpublic class AccessLimiterAspect {private static final Logger log = LoggerFactory.getLogger(AccessLimiterAspect.class); @Autowiredprivate StringRedisTemplate stringRedisTemplate; @Autowiredprivate DefaultRedisScript 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(); // 直接把当前的方法名给与keykey = 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 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!!!"); }}}

7.在需要限流的方法上进行限流测试

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 {@Autowiredprivate StringRedisTemplate stringRedisTemplate; @Autowiredprivate DefaultRedisScript limitUserAccessLua; /*** 限流的处理方法* @param userid* @return*/@GetMapping("/limit/user")public String limitUser(String userid) {// 1: 定义key是的列表List keysList = new ArrayList<>(); keysList.add("user:"+userid); // 2:执行执行lua脚本限流Boolean accessFlag = stringRedisTemplate.execute(limitUserAccessLua, keysList, "1","1"); // 3: 判断当前执行的结果,如果是0,被限制,1代表正常if (!accessFlag) {thrownew BusinessException(500,"server is busy!!!"); }return "scucess"; }/*** 限流的处理方法* @param userid* @return** 方案1:如果你的一个方法进行限流:一个方法只允许1秒100请求,key公用* 方案2:如果你的一个方法进行限流:某个用户一秒之内允许10个请求,key必须要根据参数的具体值去执行拼接。**/@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:如果你的一个方法进行限流:一个方法只允许1秒100请求,key公用* 方案2:如果你的一个方法进行限流:某个用户一秒之内允许10个请求,key必须要根据参数的具体值去执行拼接。**/@GetMapping("/limit/aop/user2")public String limitAopUser2(String userid) {return "scucess"; }}

到此这篇关于SpringBoot+Redis+Lua防止IP重复防刷攻击的方法的文章就介绍到这了,更多相关SpringBoot防止IP重复防刷 内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

    推荐阅读