springboot+vue对api请求次数的限制

gs_huang

概述

我的系统采用的是 SpringBoot + Vue 的前后端分离技术。

有一个缺陷:后端的接口暴露后,容易被不怀好意的人拿来刷接口。

为了解决这个问题,我采用了 在后端限制同一 ip 每秒的访问次数上限 的方法暂时性解决。

基本思想:

在后端记录每个 ip 当前秒的访问次数。定义一个拦截器拦截所有请求,每次接收到请求后先判断该 ip 是否已经达到当前秒的请求上限,如果未达到,正常放行且该 ip 当前秒请求数量加一,如果已达到,直接拦截掉并告知前端请求频繁。

前端判断出该问题后会延迟一秒重新发起请求频繁的请求。通过相应拦截器来判断相应状态,如果是请求频繁,则延迟一秒重新发起请求。

后端

后端主要完成对每秒请求次数的限制

存储每个 ip 请求信息的辅助类:

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
package cn.itsheng.labshow.util;

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import java.util.Date;
import java.util.HashMap;
import java.util.Map;

/**
* 用于限制 ip 访问的数据存储和设置类
* @author gs_huang
* @date 2023/11/10 16:01
*/
@Component
@Slf4j
public class IpVisitUtil {

/**
* 同一 ip 每秒最大访问次数
*/
private static Long timesOneSecond;

/**
* 使用 @Value 配合 set 方法来给静态变量注入数据
* @param timesOneSecond 配置文件中的每秒请求上限
*/
@Value("${visit.limit}")
private void setTimesOneSecond(Long timesOneSecond){
IpVisitUtil.timesOneSecond = timesOneSecond;
}

/**
* 存储每一个 ip 在某一秒的请求次数
*/
private static final Map<String, Map<Long, Long>> ipLimitMap = new HashMap<>();

/**
* 处理新的请求,判断和处理是否可以放行
* @param ipAddr 新请求的 ip 地址
* @return 是否可以放行
*/
public static boolean addVisit(String ipAddr){

log.info("已经获取当前访问者的 ip: {}", ipAddr);

// 获取当前秒的值(时间戳/1000)
Long second = new Date().getTime()/1000;

log.info("每秒最大访问次数: {}", timesOneSecond);
log.info("获取当前秒的值(时间戳/1000):{}", second);

// 保证线程安全
synchronized (IpVisitUtil.class){
Map<Long, Long> ipMap = ipLimitMap.getOrDefault(ipAddr, new HashMap<>());
if (ipMap.containsKey(second)){

log.info("已经访问次数:{}", ipMap.get(second));

Long visitTimes = ipMap.get(second) + 1;

// 访问次数大于最大访问次数
if (visitTimes > timesOneSecond){
return false;
}

ipMap.put(second, visitTimes);
} else {
ipMap.clear();
ipMap.put(second, 1L);
}

ipLimitMap.put(ipAddr, ipMap);
return true;
}
}
}

注意这里使用了 synchronized 来保证线程安全。

配置文件中对每秒请求上限的配置:

1
2
visit:
limit: 3 # 同一 ip 每秒访问次数限制(这里只是方便测试,所以设置的值比较小)

拦截器:

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
package cn.itsheng.labshow.interceptor;

import cn.hutool.extra.servlet.ServletUtil;
import cn.itsheng.labshow.util.IpVisitUtil;
import com.alibaba.fastjson.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
* @author gs_huang
* @date 2023/11/10 15:49
*/
@Component
@Slf4j
public class VisitLimitInterceptor implements HandlerInterceptor {

@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

response.setHeader("Content-Type","application/json;charset=UTF-8");

if (!IpVisitUtil.addVisit(ServletUtil.getClientIP(request))){
JSONObject json= new JSONObject();
json.put("msg", "请求过于频繁");
json.put("code",406);
response.getWriter().append(json.toJSONString());
log.info("请求过于频繁,未通过身份拦截器");
return false;
};
return true;
}
}

这里使用了 Hutool 的 ServletUtil.getClientIP() 方法来获取请求者 ip:Servlet工具-ServletUtil | Hutool

注册拦截器:

1
2
3
4
5
6
7
8
9
10
11
12
@Configuration
public class AppConfig implements WebMvcConfigurer {

@Autowired
VisitLimitInterceptor visitLimitInterceptor;


@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(visitLimitInterceptor).addPathPatterns("/**");
}
}

到这里已经完成了后端的限制请求次数工作。

但是前端有时的确会在一秒内有较高的请求数量,所以前端也要处理请求频繁失败后的请求重试工作。

前端

前端主要解决因请求频繁而失败时的请求重试工作

axios 的相应拦截器(中间件):

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
import axios from 'axios'
import router from '@/router'
import { Message } from "element-ui";
import { removeUser, setToken, getToken, removeToken } from "@/util/auth"

const service = axios.create({
baseURL: "http://localhost:8889/api",
timeout: 50000
})

service.interceptors.request.use(
config => {
config.headers['token'] = getToken()
return config
}
)

service.interceptors.response.use(
resp => {

// 请求频繁,需要一秒后重试的代码核心
if (resp.data.code === 406) {
console.log("请求过于频繁,一秒后重试请求...");

return new Promise((resolve, reject) => {
setTimeout(() => {
return service.request(resp.config).then(res => {
resolve(res)
})
}, 1000)
}).then(r => {
return r
})
}

if (resp.headers['content-type'] !== "application/json;charset=UTF-8" || !resp.config.url.includes("/admin/")) {
return resp
}

if (resp.data.code === 401) {
router.push('/login')
removeUser()
removeToken()
}

if (resp.data.code !== 200) {
Message.error(resp.data.msg);
}


if (resp.data.code === 200) {
Message({
type: 'success',
message: resp.data.msg
})
}

if ("token" in resp.headers) {
setToken(resp.headers["token"])
}

return resp

},

error => {
console.log(error)
return Promise.reject(error)
}
)

export default service

这里延迟一秒重新发送的核心:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 请求频繁,需要一秒后重试的代码核心
if (resp.data.code === 406) {
console.log("请求过于频繁,一秒后重试请求...");

return new Promise((resolve, reject) => {
setTimeout(() => {
return service.request(resp.config).then(res => {
resolve(res)
})
}, 1000)
}).then(r => {
return r
})
}

这里使用了 Promise 和 setTimeout。

setTimeout 负责延迟一秒发送请求,Promise 负责处理延迟后的返回值。

最终效果

请求失败:

api-times-limit-request-retry-fail.png

重复请求成功:

api-times-limit-request-retry-success.png

  • 标题: springboot+vue对api请求次数的限制
  • 作者: gs_huang
  • 创建于: 2023-11-11 11:41:16
  • 更新于: 2023-11-11 11:45:55
  • 链接: https://blog.itsheng.cn/2023/11/11/springboot-vue对api请求次数的限制/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
 评论
此页目录
springboot+vue对api请求次数的限制