admin 管理员组文章数量: 1086055
接口防刷处理方案
1、 API接口防刷 1.1、概念
顾名思义,就是要实现某个接口在某段时间内只能让某人访问指定次数,超出次数,就不让访问了 1.2、原理
在请求的时候,服务器通过 Redis 记录下你请求的次数,如果次数超过限制就不给访问在 Redis 保存的 Redis 是有时效性的,过期就会删除
1.3、目的
主要防止短时间接口被大量调用(攻击),出现系统崩溃和系统爬虫问题,提升服务的可用性
1.4、实现方案介绍
1. 拦截器+自定义注解+Redis
2. AOP+自定义注解+Redis
2、方案一
好,接下来我们直接实战编码,运用到项目中的话,非常实用,使用起来也非常方便,下面是我们需要
写的几个核心类
1. 自定义注解
2. 拦截器(核心)
3. Redis配置类(设置序列化用)
2.1、自定义注解
2.2、拦截器
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.util.concurrent.TimeUnit;
/**
* 防刷限流的拦截器
* @author wujiangbo
* @date 2022-08-23 18:39
*/ @Component
public class RateLimitInterceptor implements HandlerInterceptor {
@Resource private RedisTemplate<String, Integer> redisTemplate;
@Override
public boolean preHandle(
HttpServletRequest request,
HttpServletResponse response,
Object handler) throws Exception {
// 如果请求的是方法,则需要做校验
if (handler instanceof HandlerMethod) {
HandlerMethod handlerMethod = (HandlerMethod) handler;
// 获取目标方法上是否有指定注解
RateLimit rateLimit = handlerMethod.getMethodAnnotation(RateLimit.class);
if (rateLimit == null) {
//说明目标方法上没有 RateLimit 注解
return true;
}
//代码执行到此,说明目标方法上有 RateLimit 注解,所以需要校验这个请求是不是在刷接口
// 获取请求IP地址
String ip = getIpAddr(request);
// 请求url路径
String uri = request.getRequestURI();
//存到redis中的key
String key = "RateLimit:" + ip + ":" + uri;
// 缓存中存在key,在限定访问周期内已经调用过当前接口
if (redisTemplate.hasKey(key)) {
// 访问次数自增1
redisTemplate.opsForValue().increment(key, 1);
// 超出访问次数限制
if (redisTemplate.opsForValue().get(key) > rateLimit.count()) {
throw new MyException(rateLimit.msg());
}
// 未超出访问次数限制,不进行任何操作,返回true
} else {
// 第一次设置数据,过期时间为注解确定的访问周期
redisTemplate.opsForValue().set(key, 1, rateLimit.cycle(),
TimeUnit.SECONDS);
}
return true;
}
//如果请求的不是方法,直接放行
2.3、Redis配置类
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(LettuceConnectionFactory lettuceConnectionFactory) {
// 设置序列化
Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new
Jackson2JsonRedisSerializer<>(Object.class);
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(om);
// 配置redisTemplate
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(lettuceConnectionFactory);
RedisSerializer<?> stringSerializer = new StringRedisSerializer();
redisTemplate.setKeySerializer(stringSerializer);// key序列化
redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);// value序列化
redisTemplate.setHashKeySerializer(stringSerializer);// Hash key序列化
redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);//
Hash value序列化
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
}
2.4、注册拦截器
2.5、测试
打开浏览器访问:http://localhost:8081/test/test001 开始返回结果:
但是当你多刷几次后,就显示报错信息了:
很显然,我们接口防刷功能就实现了,测试成功
2.6、Lua脚本实现方案
针对上面方案我们可以改用lua脚本实现,关于lua介绍:
lua本身就是一种编程语言,是一个小巧的脚本语言
性能非常高
我们在Redis的场景中使用lua脚本有以下好处:
1. 减少网络开销: 不使用 Lua 的代码需要向 Redis 发送多次请求, 而脚本只需一次即可, 减少网络传输
2. 原子操作: Redis 将整个脚本作为一个原子执行, 无需担心并发, 也就无需事务
3. 复用: 脚本会永久保存 Redis 中, 其他客户端可继续使用
这里我们的需求是做限流,思路是根据用户的IP和访问的URI来进行计数,达到一定数量之后进行限制访
问。这应该是限流操作的计算法,另外还有令牌算法和漏桶算法
我们这里介绍最简单的计算法,首先我们在项目的resources目录下新建limit.lua文件,里面内容如下:
上面就是我们的lua限流脚本
然后我们Redis配置类中新增下面方法,用来读取上面的lua脚本:
然后拦截器类代码改成下面这样了:
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.util.Collections; import java.util.List;
/**
* 防刷限流的拦截器
* @author wujiangbo
* @date 2022-08-23 18:39
*/ @Component
public class RateLimitInterceptor implements HandlerInterceptor {
@Resource private RedisTemplate<String, Integer> redisTemplate;
@Autowired
private DefaultRedisScript<Number> redisLuaScript;
@Override
public boolean preHandle(
HttpServletRequest request,
HttpServletResponse response,
Object handler) throws Exception {
// 如果请求的是方法,则需要做校验
if (handler instanceof HandlerMethod) {
HandlerMethod handlerMethod = (HandlerMethod) handler;
// 获取目标方法上是否有指定注解
RateLimit rateLimit =
handlerMethod.getMethodAnnotation(RateLimit.class);
if (rateLimit == null) {
//说明目标方法上没有 RateLimit 注解
return true;
}
//代码执行到此,说明目标方法上有 RateLimit 注解,所以需要校验这个请求是不是在刷接口
// 获取请求IP地址
String ip = getIpAddr(request);
// 请求url路径
String uri = request.getRequestURI();
//存到redis中的key
String key = rateLimit.key() + ip + ":" + uri;
//将key转成List类型
List<String> keys = Collections.singletonList(key);
Number number = redisTemplate.execute(redisLuaScript, keys, rateLimit.count(), rateLimit.cycle());
if (number != null && number.intValue() != 0 && number.intValue() <= rateLimit.count()) {
System.out.println(rateLimit.cycle() + "秒内访问第:" + number.toString() + " 次" + getCurrentTime());
测试代码:
然后浏览器再访问做测试,就可以实现4秒内只能访问两次接口了
3、方案二
AOP方案需要导入依赖:
3.1、自定义注解
3.2、切面类
public Object around(ProceedingJoinPoint pjp) throws Throwable {
// 获取被注解的方法
MethodInvocationProceedingJoinPoint mjp =
(MethodInvocationProceedingJoinPoint) pjp;
MethodSignature signature = (MethodSignature) mjp.getSignature();
Method method = signature.getMethod();
// 获取方法上的注解
RateLimit rateLimit = method.getAnnotation(RateLimit.class);
if (rateLimit == null) {
// 如果没有注解,则继续调用,不做任何处理
return pjp.proceed();
}
/**
* 代码走到这里,说明有 RateLimit 注解,那么就需要做限流校验了
* 1、这里可以使用Redis的API做计数校验
* 2、这里也可以使用Lua脚本做计数校验,都可以
*/
//获取request对象
ServletRequestAttributes attributes = (ServletRequestAttributes)
RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
// 获取请求IP地址
String ip = getIpAddr(request);
// 请求url路径
String uri = request.getRequestURI();
//存到redis中的key
String key = "RateLimit:" + ip + ":" + uri;
// 缓存中存在key,在限定访问周期内已经调用过当前接口
if (redisTemplate.hasKey(key)) {
// 访问次数自增1
redisTemplate.opsForValue().increment(key, 1);
// 超出访问次数限制
if (redisTemplate.opsForValue().get(key) > rateLimit.count()) {
throw new MyException(rateLimit.msg());
}
// 未超出访问次数限制,不进行任何操作,返回true
} else {
// 第一次设置数据,过期时间为注解确定的访问周期
redisTemplate.opsForValue().set(key, 1, rateLimit.cycle(),
TimeUnit.SECONDS);
}
return pjp.proceed();
}
//获取请求的归属IP地址
private String getIpAddr(HttpServletRequest request) {
String ipAddress = null;
try {
ipAddress = request.getHeader("x-forwarded-for");
if (ipAddress == null || ipAddress.length() == 0 ||
"unknown".equalsIgnoreCase(ipAddress)) {
ipAddress = request.getHeader("Proxy-Client-IP");
}
3.3、测试
然后浏览器再访问做测试,就可以实现4秒内只能访问两次接口了
4、限流算法介绍(了解)
4.1、令牌桶算法
令牌桶算法是一个存放固定容量令牌的桶,按照固定速率往桶里添加令牌
桶中存放的令牌数有最大上限,超出之后就被丢弃或者拒绝
当流量或者网络请求到达时,每个请求都要获取一个令牌,如果能够获取到,则直接处理,并且令牌桶删除一个令牌。如果获取不到,该请求就要被限流,要么直接丢弃,要么在缓冲区等待
4.2、漏桶算法
漏桶算法的实现往往依赖于队列,请求到达如果队列未满则直接放入队列,然后有一个处理器按照
固定频率从队列头取出请求进行处理
如果请求量大,则会导致队列满,那么新来的请求就会被抛弃
5、总结
1. 实际项目中,接口防刷是一个非常普遍的需求
2. 一般的处理方案都是采用自定义注解+拦截器+Redis处理的
本文标签: 接口防刷处理方案
版权声明:本文标题:接口防刷处理方案 内容由网友自发贡献,该文观点仅代表作者本人, 转载请联系作者并注明出处:http://www.roclinux.cn/b/1687940901a160064.html, 本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容,一经查实,本站将立刻删除。
发表评论