Skip to content

Commit 879ca68

Browse files
committed
feat:增加请求频率限制
1 parent 8eea2aa commit 879ca68

File tree

9 files changed

+255
-1
lines changed

9 files changed

+255
-1
lines changed

README.md

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,4 +26,19 @@ base62:MurmurHash 生成的哈希值最长有 10 位十进制数,为了进
2626

2727
Redis:生成短链接后,通常在后续一段时间内此短链接的使用频率较高,则向 Redis 中添加带过期时间的缓存来减轻数据库压力。
2828

29-
302 状态码:301 为永久重定向、302 为临时重定向,通常需要记录短链接访问次数或需要修改、删除短链接时,使用 302 临时重定向来处理,和服务器压力相比,数据的价值往往更大。
29+
302 状态码:301 为永久重定向、302 为临时重定向,通常需要记录短链接访问次数或需要修改、删除短链接时,使用 302 临时重定向来处理,和服务器压力相比,数据的价值往往更大。
30+
31+
32+
33+
## 声明
34+
35+
本在线网站只用于项目展示,随时可能关闭,并不保证绝对的可用性,切勿用于商业用途或非法传播,因此产生的任何纠纷与本人无关。
36+
37+
近期服务器即将到期,正在迁移服务器数据,发现有一些异常的数据:
38+
39+
![](./snapshoot2.png)
40+
41+
我不知道是测试或是什么用途(不止这一类URL),还是很感谢这个频率😋,决定加上请求频率限制。
42+
43+
由于最近迁移服务器,将会丢失部分数据(镜像打的早,DNS 解析 TTL)。
44+

pom.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,11 @@
6262
<artifactId>spring-boot-starter-test</artifactId>
6363
<scope>test</scope>
6464
</dependency>
65+
<dependency>
66+
<groupId>org.apache.commons</groupId>
67+
<artifactId>commons-lang3</artifactId>
68+
<version>3.11</version>
69+
</dependency>
6570
</dependencies>
6671

6772
<build>

snapshoot2.png

74.9 KB
Loading
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package top.naccl.dwz.annotation;
2+
3+
import java.lang.annotation.ElementType;
4+
import java.lang.annotation.Retention;
5+
import java.lang.annotation.RetentionPolicy;
6+
import java.lang.annotation.Target;
7+
8+
/**
9+
* @Description: 访问控制
10+
* @Author: Naccl
11+
* @Date: 2021-09-16
12+
*/
13+
@Target(ElementType.METHOD)
14+
@Retention(RetentionPolicy.RUNTIME)
15+
public @interface AccessLimit {
16+
/**
17+
* 限制周期(秒)
18+
*/
19+
int seconds();
20+
21+
/**
22+
* 规定周期内限制次数
23+
*/
24+
int maxCount();
25+
26+
/**
27+
* 触发限制时的消息提示
28+
*/
29+
String msg() default "操作频率过高";
30+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package top.naccl.dwz.config;
2+
3+
import org.springframework.beans.factory.annotation.Autowired;
4+
import org.springframework.context.annotation.Configuration;
5+
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
6+
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
7+
import top.naccl.dwz.interceptor.AccessLimitInterceptor;
8+
9+
/**
10+
* @Description: 配置CORS跨域支持、拦截器
11+
* @Author: Naccl
12+
* @Date: 2020-09-16
13+
*/
14+
@Configuration
15+
public class WebConfig implements WebMvcConfigurer {
16+
@Autowired
17+
AccessLimitInterceptor accessLimitInterceptor;
18+
19+
@Override
20+
public void addInterceptors(InterceptorRegistry registry) {
21+
registry.addInterceptor(accessLimitInterceptor);
22+
}
23+
}

src/main/java/top/naccl/dwz/controller/IndexController.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import org.springframework.web.bind.annotation.PostMapping;
99
import org.springframework.web.bind.annotation.RequestParam;
1010
import org.springframework.web.bind.annotation.ResponseBody;
11+
import top.naccl.dwz.annotation.AccessLimit;
1112
import top.naccl.dwz.entity.R;
1213
import top.naccl.dwz.service.UrlService;
1314
import top.naccl.dwz.util.HashUtils;
@@ -34,6 +35,7 @@ public String index() {
3435
return "index";
3536
}
3637

38+
@AccessLimit(seconds = 10, maxCount = 1, msg = "10秒内只能生成一次短链接")
3739
@PostMapping("/generate")
3840
@ResponseBody
3941
public R generateShortURL(@RequestParam String longURL) {
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
package top.naccl.dwz.interceptor;
2+
3+
import org.springframework.beans.factory.annotation.Autowired;
4+
import org.springframework.data.redis.core.StringRedisTemplate;
5+
import org.springframework.stereotype.Component;
6+
import org.springframework.web.method.HandlerMethod;
7+
import org.springframework.web.servlet.HandlerInterceptor;
8+
import top.naccl.dwz.annotation.AccessLimit;
9+
import top.naccl.dwz.entity.R;
10+
import top.naccl.dwz.util.IpAddressUtils;
11+
import top.naccl.dwz.util.JacksonUtils;
12+
13+
import javax.servlet.http.HttpServletRequest;
14+
import javax.servlet.http.HttpServletResponse;
15+
import java.io.PrintWriter;
16+
import java.util.concurrent.TimeUnit;
17+
18+
/**
19+
* @Description: 访问控制拦截器
20+
* @Author: Naccl
21+
* @Date: 2021-09-16
22+
*/
23+
@Component
24+
public class AccessLimitInterceptor implements HandlerInterceptor {
25+
@Autowired
26+
StringRedisTemplate redisTemplate;
27+
28+
@Override
29+
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
30+
if (handler instanceof HandlerMethod) {
31+
HandlerMethod handlerMethod = (HandlerMethod) handler;
32+
AccessLimit accessLimit = handlerMethod.getMethodAnnotation(AccessLimit.class);
33+
//方法上没有访问控制的注解,直接通过
34+
if (accessLimit == null) {
35+
return true;
36+
}
37+
int seconds = accessLimit.seconds();
38+
int maxCount = accessLimit.maxCount();
39+
String ip = IpAddressUtils.getIpAddress(request);
40+
String method = request.getMethod();
41+
String requestURI = request.getRequestURI();
42+
43+
String redisKey = ip + ":" + method + ":" + requestURI;
44+
Object redisResult = redisTemplate.opsForValue().get(redisKey);
45+
Integer count = JacksonUtils.convertValue(redisResult, Integer.class);
46+
if (count == null) {
47+
//在规定周期内第一次访问,存入redis
48+
redisTemplate.opsForValue().increment(redisKey, 1);
49+
redisTemplate.expire(redisKey, seconds, TimeUnit.SECONDS);
50+
} else {
51+
if (count >= maxCount) {
52+
//超出访问限制次数
53+
response.setContentType("application/json;charset=utf-8");
54+
PrintWriter out = response.getWriter();
55+
R result = R.create(403, accessLimit.msg());
56+
out.write(JacksonUtils.writeValueAsString(result));
57+
out.flush();
58+
out.close();
59+
return false;
60+
} else {
61+
//没超出访问限制次数
62+
redisTemplate.opsForValue().increment(redisKey, 1);
63+
}
64+
}
65+
}
66+
return true;
67+
}
68+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
package top.naccl.dwz.util;
2+
3+
import lombok.extern.slf4j.Slf4j;
4+
import org.springframework.stereotype.Component;
5+
import org.apache.commons.lang3.StringUtils;
6+
7+
import javax.servlet.http.HttpServletRequest;
8+
import java.net.InetAddress;
9+
import java.net.UnknownHostException;
10+
11+
/**
12+
* @Description: ip记录
13+
* @Author: Naccl
14+
* @Date: 2020-08-18
15+
*/
16+
@Slf4j
17+
@Component
18+
public class IpAddressUtils {
19+
/**
20+
* 在Nginx等代理之后获取用户真实IP地址
21+
*
22+
* @param request
23+
* @return
24+
*/
25+
public static String getIpAddress(HttpServletRequest request) {
26+
String ip = request.getHeader("x-forwarded-for");
27+
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
28+
ip = request.getHeader("Proxy-Client-IP");
29+
}
30+
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
31+
ip = request.getHeader("WL-Proxy-Client-IP");
32+
}
33+
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
34+
ip = request.getHeader("HTTP_CLIENT_IP");
35+
}
36+
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
37+
ip = request.getHeader("HTTP_X_FORWARDED_FOR");
38+
}
39+
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
40+
ip = request.getRemoteAddr();
41+
if ("127.0.0.1".equals(ip) || "0:0:0:0:0:0:0:1".equals(ip)) {
42+
//根据网卡取本机配置的IP
43+
InetAddress inet = null;
44+
try {
45+
inet = InetAddress.getLocalHost();
46+
} catch (UnknownHostException e) {
47+
log.error("getIpAddress exception:", e);
48+
}
49+
ip = inet.getHostAddress();
50+
}
51+
}
52+
return StringUtils.substringBefore(ip, ",");
53+
}
54+
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
package top.naccl.dwz.util;
2+
3+
import com.fasterxml.jackson.core.JsonProcessingException;
4+
import com.fasterxml.jackson.core.type.TypeReference;
5+
import com.fasterxml.jackson.databind.ObjectMapper;
6+
7+
import java.io.IOException;
8+
import java.io.InputStream;
9+
10+
/**
11+
* @Description: Jackson Object Mapper
12+
* @Author: Naccl
13+
* @Date: 2020-11-07
14+
*/
15+
public class JacksonUtils {
16+
private static ObjectMapper objectMapper = new ObjectMapper();
17+
18+
public static String writeValueAsString(Object value) {
19+
try {
20+
return objectMapper.writeValueAsString(value);
21+
} catch (JsonProcessingException e) {
22+
e.printStackTrace();
23+
return "";
24+
}
25+
}
26+
27+
public static <T> T readValue(String content, Class<T> valueType) {
28+
try {
29+
return objectMapper.readValue(content, valueType);
30+
} catch (IOException e) {
31+
e.printStackTrace();
32+
return null;
33+
}
34+
}
35+
36+
public static <T> T readValue(String content, TypeReference<T> valueTypeRef) {
37+
try {
38+
return objectMapper.readValue(content, valueTypeRef);
39+
} catch (IOException e) {
40+
e.printStackTrace();
41+
return null;
42+
}
43+
}
44+
45+
public static <T> T readValue(InputStream src, Class<T> valueType) {
46+
try {
47+
return objectMapper.readValue(src, valueType);
48+
} catch (IOException e) {
49+
e.printStackTrace();
50+
return null;
51+
}
52+
}
53+
54+
public static <T> T convertValue(Object fromValue, Class<T> toValueType) {
55+
return objectMapper.convertValue(fromValue, toValueType);
56+
}
57+
}

0 commit comments

Comments
 (0)