SpringBoot+Redis+Lua防止IP重复防刷攻击的方法

网友投稿 362 2022-09-07


SpringBoot+Redis+Lua防止IP重复防刷攻击的方法

黑客或者一些恶意的用户为了攻击你的网站或者APP。通过肉机并发或者死循环请求你的接口。从而导致系统出现宕机。

针对新增数据的接口,会出现大量的重复数据,甚至垃圾数据会将你的数据库和CPU或者内存磁盘耗尽,直到数据库撑爆为止。

针对查询的接口。黑客一般是重点攻击慢查询,比如一个SQL是2S。只要黑客一致攻击,就必然造成系统被拖垮,数据库查询全都被阻塞,连接一直得不到释放造成数据库无法访问。

具体要实现和达到的效果是:

需求:在10秒内,同一IP 127.0.0.1 地址只允许访问30次。

最终达到的效果:

Long execute = this.stringReFqccBeAdisTemplate.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 0

end

return 1

第二步:创建lua对象

@SpringBootConfiguration

public class LuaConfiguration {

/**

* 将lua脚本的内容加载出来放入到DefaultRedisScript

* @return

*/

@Bean

public 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

*/

@RestController

public class IpLuaController {

private static final Logger log = LoggerFactory.getLogger(IpLuaController.class);

@Autowired

private StringRedisTemplate stringRedisTemplate;

@Autowired

private 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.boot

spring-boot-starter-aop

org.springframework.boot

spring-boot-starter-web

org.springframework.boot

spring-boot-starter-test

test

org.springframework.boot

spring-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 then

redis.call("expire",KEYS[1],ARGV[1])

end

-- 如果次数还没有过期,并且还在规定的次数内,说明还在请求同一接口

if limitCount > tonumber(ARGV[2]) then

return 0

end

return 1

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.http://ResourceScriptSource;

/**

* @author 飞哥

* @Title: 学相伴出品

* @Description: 我们有一个学习网站:https://kuangstudy.com

* @date 2021/5/21 12:01

*/

@Configuration

public class LuaConfiguration {

/**

* 将lua脚本的内容加载出来放入到DefaultRedisScript

* @return

*/

@Bean

public DefaultRedisScript limitUserAccessLua() {

// 1: 初始化一个lua脚本的对象DefaultRedisScript

DefaultRedisScript 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;

}

}

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://kuangstudy.com

* @date 2021/5/20 13:16

*/

@Configuration

public class RedisConfiguration {

/**

* @return org.springframework.data.redis.core.RedisTemplate

* @Description 改写redistemplate序列化规则

**/

@Bean

public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory) {

// 1: 开始创建一个redistemplate

RedisTemplate redisTemplate = new RedisTemplate<>();

// 2:开始redis连接工厂跪安了

redisTemplate.setConnectionFactory(redisConnectionFactory);

// 创建一个json的序列化方式

GenericJackson2JsonRedisSerializer jackson2JsonRedisSerializer = new GenericJackson2JsonRedisSerializer();

// 设置key用string序列化方式

redisTemplate.setKeySerializer(new StringRedisSerializer());

// 设置value用jackjFqccBeAson进行处理

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)

@Documented

public @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;

}

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 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 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;

@RestController

public class RateLimiterController {

@Autowired

private StringRedisTemplate stringRedisTemplate;

@Autowired

private 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) {

throw new 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";

}

}


版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系我们jiasou666@gmail.com 处理,核实后本网站将在24小时内删除侵权内容。

上一篇:【打靶识别】基于计算机视觉实现自动报靶系统附matlab代码
下一篇:【PRM路径规划】基于RRT算法避障路径规划附matlab代码
相关文章

 发表评论

暂时没有评论,来抢沙发吧~