SpringBoot对接口限制IP访问次数
原文作者:钟小嘿
原文链接:https://www.cnblogs.com/zys2019/p/16328053.html
对于某些特定的接口,为了防止数据碰撞等问题,可限制接口对同一IP在一段时间内的访问次数。本文使用注解方式:
1、导入需要的依赖
<!-- redis依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- AOP依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<!-- Map依赖 -->
<dependency>
<groupId>net.jodah</groupId>
<artifactId>expiringmap</artifactId>
<version>0.5.8</version>
</dependency>
2、定义注解
定义注解InterfaceLimit,用于接口拦截
package com.zxh.example.anno;
import java.lang.annotation.*;
/**
* 接口访问频率注解,默认一分钟只能访问5次
*/
@Documented
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface InterfaceLimit {
long time() default 60000; // 限制时间 单位:毫秒(默认值:一分钟)
int value() default 5; // 允许请求的次数(默认值:5次)
}
3、在切面做限制
在切面中对接口进行限制。
1)服务为单节点
package com.zxh.example.anno;
import lombok.extern.slf4j.Slf4j;
import net.jodah.expiringmap.ExpirationPolicy;
import net.jodah.expiringmap.ExpiringMap;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
@Aspect
@Component
@Slf4j
public class InterfaceLimitAspect {
private static ConcurrentHashMap<String, ExpiringMap<String, Integer>> book = new ConcurrentHashMap<>();
@Autowired
private StringRedisTemplate redisTemplate;
/**
* 层切点
*/
@Pointcut("@annotation(interfaceLimit)")
public void controllerAspect(InterfaceLimit interfaceLimit) {
}
@Around("controllerAspect(interfaceLimit)")
public Object doAround(ProceedingJoinPoint pjp, InterfaceLimit interfaceLimit) throws Throwable {
// 获得request对象
RequestAttributes ra = RequestContextHolder.getRequestAttributes();
ServletRequestAttributes sra = (ServletRequestAttributes) ra;
HttpServletRequest request = sra.getRequest();
// 获取Map value对象, 如果没有则返回默认值
// getOrDefault获取参数,获取不到则给默认值
ExpiringMap<String, Integer> uc = book.getOrDefault(request.getRequestURI(), ExpiringMap.builder().variableExpiration().build());
Integer uCount = uc.getOrDefault(request.getRemoteAddr(), 0);
if (uCount >= interfaceLimit.value()) { // 超过次数,不执行目标方法
log.error("接口拦截:{} 请求超过限制频率【{}次/{}ms】,IP为{}", request.getRequestURI(), interfaceLimit.value(), interfaceLimit.time(), request.getRemoteAddr());
return "请求过于频繁,请稍后再试";
} else if (uCount == 0) { // 第一次请求时,设置有效时间
uc.put(request.getRemoteAddr(), uCount + 1, ExpirationPolicy.CREATED, interfaceLimit.time(), TimeUnit.MILLISECONDS);
} else { // 未超过次数, 记录加一
uc.put(request.getRemoteAddr(), uCount + 1);
}
book.put(request.getRequestURI(), uc);
// result的值就是被拦截方法的返回值
Object result = pjp.proceed();
return result;
}
}
使用ConcurrentHashMap和ExpiringMap来对请求路径和ip进行限制。其中ConcurrentHashMap可以处理并发情况的 HashMap,ExpiringMap为单个元素设置过期时间。
2)服务为多节点(推荐)
由于ConcurrentHashMap是基于线程的,当服务为多节点时,只能在当前服务有效,那么就会造成实际接口的限制大于规定的限制。
这时可借助redis进行限制。验证的方法修改如下:
@Around("controllerAspect(interfaceLimit)")
public Object doAround(ProceedingJoinPoint pjp, InterfaceLimit interfaceLimit) throws Throwable {
// 获得request对象
RequestAttributes ra = RequestContextHolder.getRequestAttributes();
ServletRequestAttributes sra = (ServletRequestAttributes) ra;
HttpServletRequest request = sra.getRequest();
//redis这里推荐使用hash类型,url为外层key,ip作为内层key,访问次数作为value
BoundHashOperations<String, Object, Object> ops = redisTemplate.boundHashOps("test:interfaceLimit:" + request.getRequestURI());
String ipCnt = (String) ops.get(request.getRemoteAddr());
Integer uCount = ipCnt == null ? 0 : "".equals(ipCnt) ? 0 : Integer.parseInt(ipCnt);
if (uCount >= interfaceLimit.value()) { // 超过次数,不执行目标方法
log.error("接口拦截:{} 请求超过限制频率【{}次/{}ms】,IP为{}", request.getRequestURI(), interfaceLimit.value(), interfaceLimit.time(), request.getRemoteAddr());
return "请求过于频繁,请稍后再试";
} else {
//请求时,设置有效时间, 记录加一
ops.increment(request.getRemoteAddr(), 1);
ops.expire(interfaceLimit.time(), TimeUnit.MILLISECONDS);
}
// result的值就是被拦截方法的返回值
Object result = pjp.proceed();
return result;
}
4、在接口中使用注解
package com.zxh.example.controller;
import com.zxh.example.anno.InterfaceLimit;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api")
@Slf4j
public class TestController {
@InterfaceLimit
@GetMapping("/test")
public String test() {
return "123";
}
@InterfaceLimit(value = 10)
@GetMapping("/test2")
public String test2() {
return "1234";
}
}
可使用默认的访问频率,也可自行传值。
本地启动后在1分钟内连续访问/api/test接口(/api/test2同),当未超过5次时,访问正常,超过5次时,就显示请求过于频繁,请稍后再试。控制台打印结果:
就是这么简单,你学废了吗?感觉有用的话,点个赞吧 !
标题:SpringBoot对接口限制IP访问次数
作者:jerrycookie
地址:https://www.mmzsblog.cn/articles/2023/07/19/1689754387536.html
如未加特殊说明,文章均为原创,转载必须注明出处。均采用CC BY-SA 4.0 协议!
本网站发布的内容(图片、视频和文字)以原创、转载和分享网络内容为主,如果涉及侵权请尽快告知,我们将会在第一时间删除。若本站转载文章遗漏了原文链接,请及时告知,我们将做删除处理!文章观点不代表本网站立场,如需处理请联系首页客服。• 网站转载须在文章起始位置标注作者及原文连接,否则保留追究法律责任的权利。
• 公众号转载请联系网站首页的微信号申请白名单!