接口防刷
思路
- 通过Redis进行限制。
- 用户在访问接口的时候,获取用户的ip:
$request -> ip();
- 把ip作为key,访问次数作为对应的值,每次访问接口累加访问次数即可。
- key的有效期是1分钟必须是第一次写入的时候 指定1分钟的有效期
- 下一次访问的时候,判断是否超过100次,如果超过的,吧ip加入黑名单中半个小时。
- 用的数据类型是有序集合
- 用户再访问的时候,判断他的ip是否在黑名单中,如果在黑名单,就不允许访问
- 需要注意,我们只是吧这个ip放入黑名单半个小时。如果超过半个小时 需要从黑名单移除这个ip
存在的问题
一分钟统计ip访问次数,可能会存在的问题?
- 可能在有效期内,不超过访问的次数限制,但是接近这个值。
- 比如说我们在12:00:01的时候,访问1次,在12:00:59访问了98次,这样的话是没有到100次的,在12:00统计的数据,在12:01之前就过期了。
- 继续在12:01:01在去访问99次,也没有达到限制的100次。这样的话,在12:00:59到12:01:01这三秒钟超过了100次。这样的话 限制就不存在意义了。
如何解决按分钟统计出现的问题?
分开统计访问次数:
- 把一分钟分为6段,每段分别统计访问次数
- 每次再算访问次数的时候,统计出当前的时间端的访问次数
- 在向前去前5段时间的访问次数,把这6段时间加起来
- 判断是否超过我们的限制,如果超过加入黑名单
令牌桶
- 在redis中设置一个集合,集合中存在10个元素
- 客户端每次调用接口的时候,从集合中取出一个值
- 如果能够正常去到值,说明正常,允许访问
- 如果没有取到,说明限制令牌桶是空的,则不允许访问
- 每隔10s中给集合中添加10个元素 【 不是每次都写10个元素,而是把集合补够10个数字即可 】
其他思路
- 网关控制流量洪峰,对在一个时间段内出现流量异常,可以拒绝请求。
- 源
ip
请求个数限制。对请求来源的ip
请求个数做限制。 http
请求头信息校验;(例如host
,User-Agent
,Referer
)。- 对用户唯一身份uid进行限制和校验。例如基本的长度,组合方式,甚至有效性进行判断。或者uid具有一定的时效性。
- 前后端协议采用二进制方式进行交互或者协议采用签名机制。
- 人机验证,验证码,短信验证码,滑动图片形式,12306形式。
实现方案
- 首先是写一个注解类
- 拦截器中实现
- 注册到springboot中
- 在Controller中加入注解
注解类
代码如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22package tk.fulsun.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* @author fulsun
* @description: 访问限制注解类
* @date 6/9/2021 2:25 PM
*/
public AccessLimit {
int seconds();
int maxCount();
boolean needLogin() default true;
}
拦截器中实现
代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93package tk.fulsun.config;
import com.alibaba.fastjson.JSON;
import java.io.OutputStream;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.Semaphore;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
import tk.fulsun.annotation.AccessLimit;
import tk.fulsun.common.CodeMsg;
import tk.fulsun.utils.RedisUtil;
/**
* @author fulsun
* @description: 访问的拦截器
* @date 6/9/2021 2:28 PM
*/
public class ApiAccessInterceptor extends HandlerInterceptorAdapter {
private RedisTemplate redisTemplate;
private RedisUtil redisUtil;
private final Semaphore permit = new Semaphore(10, true);
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
redisUtil.setRedisTemplate(redisTemplate);
// 判断请求是否属于方法的请求
if (handler instanceof HandlerMethod) {
HandlerMethod hm = (HandlerMethod) handler;
// 获取方法中的注解,看是否有该注解
AccessLimit accessLimit = hm.getMethodAnnotation(AccessLimit.class);
if (accessLimit == null) {
return true;
}
int seconds = accessLimit.seconds();
int maxCount = accessLimit.maxCount();
boolean login = accessLimit.needLogin();
String key = request.getRequestURI();
// 如果需要登录
if (login) {
// 获取登录的session进行判断
// 这里假设用户id是1,项目中是动态获取的userId
key += "" + "1";
}
// 从redis中获取用户访问的次数
try {
// 控制并发数量为10
permit.acquire();
Integer count = (Integer) redisUtil.get(key);
System.out.println("当前次数 " + count);
if (count == null) {
// 第一次访问
redisUtil.set(key, 1, seconds);
} else if (count < maxCount) {
// +1
redisUtil.incr(key, 1);
} else {
// 超出访问次数, 这里的CodeMsg是一个返回参数
render(response, CodeMsg.ACCESS_LIMIT_REACHED);
return false;
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
permit.release();
}
}
return true;
}
private void render(HttpServletResponse response, CodeMsg cm) throws Exception {
response.setContentType("application/json;charset=UTF-8");
response.setStatus(cm.getCode());
OutputStream out = response.getOutputStream();
Map result = new HashMap<>(3);
result.put("status", "1");
result.put("message", "");
result.put("error", cm.getMessage());
String str = JSON.toJSONString(result);
out.write(str.getBytes("UTF-8"));
out.flush();
out.close();
}
}
注册到springboot中
1 | package tk.fulsun.config; |
Controller中加入注解
1 | package tk.fulsun.controller; |
测试结果
使用jmeter,测试同时启动1000的并发在1秒访问 /h2 接口,得到的测试结果如下
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 凉月の博客!
评论