- 基于Springboot限制接口访问流量
为什么要限制接口访问次数
在实际开发中,我们会遇到由于恶意频繁的操作访问接口导致服务奔溃,或者在请求短信验证码时被恶意疯狂的请求接口,导致服务费增加,这是每个程序员都不想看到的,所以就有了流量控制,如何做到流量访问控制呢?这里有一个思路就是在访问接口的时候需要被拦截器拦截请求然后再到缓存或者redis中查询是否在规定时间内同同一客户端不停的访问该接口,如果达到限制就直接拦截斌返回设定好的返回值
实现接口限流操作
首先发一下一个简单的demo结构
具体思路
首先我们才用自定义注解加aop切片的方式进行接口拦截,并结合redis的方式记录访问次数
引入相关maven依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
<version>2.7.3</version>
</dependency>
yaml内容
redis:
host: 127.0.0.1
database: 0
port: 6379
password:
server:
port: 8081
创建自定义注解
package myclass.com.demo1.interfaces;
import java.lang.annotation.*;
@Documented
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface LimitQuest {
long time() default 3*60*1000;//限制时间内毫秒级(如果使用时不填就用这里默认的)
int count() default Integer.MAX_VALUE;//限制访问次数(与上同理)
}
写切片,将自定义的切点
@Aspect
@Component
这两个注解将bean注入到容器中
package myclass.com.demo1.interfaces;
import myclass.com.demo1.pojo.R;
import org.apache.catalina.util.RequestUtil;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.http.HttpRequest;
import org.springframework.stereotype.Component;
import com.google.common.collect.Maps;
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 javax.servlet.http.HttpServletResponse;
import java.util.Map;
import java.util.concurrent.TimeUnit;
/*
* 设置切片
*
* */
@Aspect
@Component
public class LimitQuestAspect {
private static final Logger logger = LoggerFactory.getLogger("LimitQuestAspect");
@Autowired
private RedisTemplate<String, String> redisTemplate;
// //定义切点
// @Pointcut("@annotation(limitQuest)")
// public void excudeService(LimitQuest limitQuest){
// }
//定义切点(此切点若在同级下类名若在不同级下采用全限定名找到要切入的点)
@Pointcut("@annotation(limitQuest)")
public void excudeService(LimitQuest limitQuest){
}
@Around("excudeService(limitQuest)")
public Object doAroud(ProceedingJoinPoint pjp, LimitQuest limitQuest) throws Throwable {
System.out.println("wojinglaile1!!!!!!!");
// StringBuffer requestURL = RequestUtil.getRequestURL(request);
ServletRequestAttributes sa= (ServletRequestAttributes)RequestContextHolder.getRequestAttributes();
HttpServletRequest request = sa.getRequest();
boolean vcode = true;
vcode=validateCode(request,limitQuest.count(),limitQuest.time());
if(!vcode){
Map<String, Object> resultMap = Maps.newHashMap();
resultMap.put("retCode", 100000001);
resultMap.put("retDesc", "请求已超过流量上限请稍后重试");
return R.error().data(resultMap);
}
//result就是被拦截方法的返回值
Object result = pjp.proceed();
return result;
}
/**
* 接口的访问频次限制
*
* @param request
* @return
*/
private boolean validateCode(HttpServletRequest request, int maxSize, long timeOut) {
boolean resultCode = true;
try {
LimitQuestAspect limitQuestAspect =new LimitQuestAspect();
String ip = limitQuestAspect.getIpAddress(request);
String url = request.getRequestURL().toString();
String key = "req_limit_".concat(url).concat(ip);
long count = redisTemplate.opsForValue().increment(key, 1);
if (count == 1) {
redisTemplate.expire(key, timeOut, TimeUnit.MILLISECONDS);
}
if (count > maxSize) {
logger.info("用户IP[" + ip + "]访问地址[" + url + "]超过了限定的次数[" + maxSize + "]");
resultCode = false;
}
} catch (Exception e) {
logger.error("发生异常: ", e);
}
return resultCode;
}
//获取客户端真是ip地址
public String getIpAddress(HttpServletRequest request) {
String ipAddress = request.getHeader("x-forwarded-for");
if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
ipAddress = request.getHeader("Proxy-Client-IP");
}
if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
ipAddress = request.getHeader("WL-Proxy-Client-IP");
}
if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
ipAddress = request.getHeader("HTTP_CLIENT_IP");
}
if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
ipAddress = request.getHeader("HTTP_X_FORWARDED_FOR");
}
if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
ipAddress = request.getRemoteAddr();
}
if (ipAddress != null && ipAddress.length() > 15 && ipAddress.indexOf(",") > 0) {
ipAddress = ipAddress.substring(0, ipAddress.indexOf(","));
}
return ipAddress;
}
}
设置公共实体返回
package myclass.com.demo1.pojo;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.util.HashMap;
import java.util.Map;
@Data
public class R {
@ApiModelProperty(value = "是否成功")
private Boolean success;
@ApiModelProperty(value = "返回码")
private Integer code;
@ApiModelProperty(value = "返回消息")
private String message;
@ApiModelProperty(value = "返回数据")
private Map<String, Object> data = new HashMap<String, Object>();
//把构造方法私有 不让别人new该类
private R() {}
//实现链式编程
//R.ok().code().message().data();
//成功静态方法
public static R ok() {
R r = new R();
r.setSuccess(true);
r.setCode(ResultCode.SUCCESS);
r.setMessage("成功");
return r;
}
//失败静态方法
public static R error() {
R r = new R();
r.setSuccess(false);
r.setCode(ResultCode.ERROR);
r.setMessage("失败");
return r;
}
public R success(Boolean success){
this.setSuccess(success);
return this; //返回this 实现链式编程
}
//实现链式编程
//R.ok().code().message().data();
public R message(String message){
this.setMessage(message);
return this;
}
public R code(Integer code){
this.setCode(code);
return this;
}
public R data(String key, Object value){
this.data.put(key, value);
return this;
}
public R data(Map<String, Object> map){
this.setData(map);
return this;
}
}
公共请求相应编码
package myclass.com.demo1.pojo;
public interface ResultCode {
public static Integer SUCCESS = 20000;
public static Integer ERROR = 20001;
}
编写测试接口实验
需要将我们的自定义注解加到对应想要限流的接口
package myclass.com.demo1;
import myclass.com.demo1.interfaces.LimitQuest;
import myclass.com.demo1.pojo.R;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/myclass/demo1")
public class controller {
@RequestMapping(value = {"getDocRecvCount"})
@LimitQuest(count = 5,time = 60000)//自定义注解次数和时间
public R getCount() {
return R.ok().data("hello"," world!!!");
}
}
在main方法中要将包下的类扫描
package myclass;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.ComponentScan;
@SpringBootApplication
@ComponentScan("myclass.com.*")//需要添加
public class SpringbootApplication {
public static void main(String[] args) {
SpringApplication.run(SpringbootApplication.class, args);
}
}
请求看看吧
超时结果
正常结果
这里转载一个redisTemplate的常用使用博主觉得听全面
https://blog.csdn.net/yang_hui_liang/article/details/97900048/