各位小伙伴们大家好!!,在平常的编写接口的过程中,一般都会遇到一个问题就是说关于我们接口限速的,如
- 同一用户恶意调用同一接口,导致接口压力过大
- 用户频繁提交的问题,有些操作是不必要的,所以我们需要对同一用户进行接口限速!!!
当然在我们整合第三方服务,如高德地图,微信小程序等等一些服务时,别人对接口的调用也做了限制如每日的调用次数,或者说QPS意思是接口每秒的响应效率等 因为设计到成本的问题,毕竟有钱就能使你变强,马爸爸说的,可不关我的事,当然实现方法有很多种,看具体的业务场景,此博文只提供一种思路或者一种实现方式!

项目实际需求点
项目是一个基于微信小程序,为一个音乐节的前期宣传推广而开发的,存在用户给选手投票,但是每个用户的投票次数都是有次数限制的,大家也知道平常我们点赞的时候会连续点击,有些时候我们没有票数了,用户还在点击(可以通过前端限制解决),或者说用户直接拿到了我们接口的URL地址直接进行访问刷新会导致很多问题!!!!
需要掌握的知识点
springBoot,Redis,AOP,注解和反射,异常
不会也没关系,注释很详细可以当入门看
封装思路
- 通过注解标识我们的接口,对于同一用户的次数限制,限制时长区间是多少,快速使用接口的限速,实现快速可拔插的效果,特别方便
- 有了注解,我们肯定需要Handle去处理,标识过的此注解的接口
- 如果用户的访问次数超过我们的限制,抛出了异常需要手动捕获吗?当然这样是很麻烦的!我们可以使用自定义异常,和Spring提供的AOP和注解来实现异常中心捕获我们的自定义异常!
上代码
1.首先定义我们的注解,用于标注在我们的接口之上,代表需要增强的接口
/**
* 创建
* ip拦截器 注解
* @author cc
* @date 2020-06-26-9:36
*/
//元注解--->注解信息保留到class文件运行仍可获取
@Retention(RetentionPolicy.RUNTIME)
//元注解--->只能使用在我们的方法之上
@Target(ElementType.METHOD)
//元注解--->JavaDoc会生成此枚举的文档
@Documented
public @interface IpInterceptor {
/**
* @Description: 限制某时间段内可以访问的次数 (默认设置100)
* @return
*/
long requestCounts() default 100;
/**
* @Description: 限制访问的某一个时间段,单位为秒 (默认值1分钟即可)
* @return
*/
int expiresTimeSecond() default 60;
/**
* @Description:
* 接口是否是restful风格 (默认false)--->因为Restful风格请求参数是直接跟在请求地址后,由于每次传递的参数不同也会导致,也就无法判断真正的请求地址,由此需要标注
*
* @return
*/
boolean isRestful() default false;
/**
* @Description: restful风格参数个数 (默认0),用于restful风格请求地址的解析
* @return
*/
int restfulParamCounts() default 0;
}2.注解的处理器 —>注解功能的具体实现
/**
* 创建
* IpInterceptor ip拦截器具体实现
* @author cc
* @date 2021-02-26-10:46
*/
//自定义类 切面
@Aspect
//注入Spring
@Component
@Slf4j
public class IpInterceptorImpl {
@Autowired
private RedisUtil redisUtil;
@Autowired
@Qualifier("myRedisTemplate")
private RedisTemplate<String, Object> redisTemplate;
//方法前置增强--->增强标注过IpInterceptor注解的方法
@Before("@annotation(com.cc.util.IpInterceptor)")
public void requestLimit(JoinPoint joinPoint) {
long incr=0;
// 1.获取HttpRequest
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
// 2.判断request不能为空 存在异常将被GlobalExceptionHandler统一捕获
if (SuperUtil.isNullOrEmpty(request)) throw new IpExcetion(IpExcetionEnum.HTTP_ERROR);
//3.获取切入方法上标识的注解信息
IpInterceptor info = this.getAnnotation(joinPoint);
//4.判断解析出来的注解对象不能为空 存在异常将被GlobalExceptionHandler统一捕获
if(SuperUtil.isNullOrEmpty(info))throw new IpExcetion(IpExcetionEnum.GET_ANNOTATION_FAIL);
//5.获取请求者的ip地址
String requestIp = request.getRemoteAddr();
//6.存储请求者请求的地址
StringBuilder requestUrl = new StringBuilder();
//7.对地址进行解析判断是否是,restful风格 如果是restful风格需要进行单独的解析,因为参数是拼接到我们的URL上的
if(info.isRestful()){
//根据/ 对请求的地址进行分割
String[] urls = request.getRequestURI().split("/");
//循环解析出请求地址 根据restfulParamCounts属性,忽略掉restful风格导致的参数,注意点--->循环要从1开始,因为url地址一开始就会有一个/
for (int i = 1; i < urls.length-info.restfulParamCounts(); i++) {
requestUrl.append("/").append(urls[i]);
}
}else{
requestUrl.append(request.getRequestURI());
}
//6.生成redisKey
StringBuffer redisKey=new StringBuffer("REQUEST_LIMIT:").append(requestUrl).append("-").append(requestIp) ;
//如果元素不为空 代表此key已设置
if(redisUtil.hasKey(redisKey.toString())){
//7.原子加 此key
incr= redisUtil.incr(redisKey.toString(), 1);
//8.如果计数大于我们的 限制次数 直接抛出异常由GlobalExceptionHandler统一捕获 403拒绝访问
if(incr>info.requestCounts()){
throw new IpExcetion(IpExcetionEnum.ACCESS_FREQUENCY_IS_TOO_FAST);
}
//9.如果元素为空 代表此key未设置 将递增操作和设置过期时间的操作放在了一个事务中,从而保证了两个操作的原子性
//不这样做 比如客户端在执行INCR之后,没有成功设置EXPIRE时间。这个ip的key 会造成内存泄漏
}else {
//开启事务支持,在同一个 Connection 中执行命令 redisKey.toString()
redisUtil.enableTransactionSupport();
//开启事务
redisUtil.multi();
//原子加
redisTemplate.opsForValue().increment(redisKey.toString());
//设置过期时间
redisTemplate.expire(redisKey.toString(), info.expiresTimeSecond(), TimeUnit.SECONDS);
redisUtil.exec();//执行
}
log.debug("IP限速地址KEY--->"+redisKey+"---次数--->"+incr);
}
/**
*
* @Description: 获取切入方法标识的注解信息
* @param joinPoint
* @return
* @author cc
*/
private IpInterceptor getAnnotation(JoinPoint joinPoint) {
//1.连接点处的签名
Signature signature = joinPoint.getSignature();
//2.强制转换成方法签名
MethodSignature methodSignature = (MethodSignature) signature;
//3.获取反射方法对象
Method method = methodSignature.getMethod();
//4.如果对象 不为空
if(method!=null){
//返回方法上标注的注解
return method.getAnnotation(IpInterceptor.class);
}
return null;
}
}3.自定义异常类,用于规范统一,抛出异常
/**
* 创建
* ip自定义异常
* @author cc
* @date 2021-02-26-11:48
*/
@Data
//继承RuntimeException是不需要显示捕获异常的
public class IpExcetion extends RuntimeException {
/**
* 异常状态码
*/
private int errorCode;
public IpExcetion(IpExcetionEnum ipExcetionEnum) {
//从枚举中获取需要的信息
super(ipExcetionEnum.getMessage());
this.errorCode = ipExcetionEnum.getCode();
}
public IpExcetion() {
}
}4.自定义异常枚举—>规范统一的抛出异常
/**
* 创建
* 异常枚举
* @author cc
* @date 2021-02-26-10:46
*/
enum IpExcetionEnum {
HTTP_ERROR("HttpServletRequest有误",400),
GET_ANNOTATION_FAIL("注解信息获取失败",500),
ACCESS_FREQUENCY_IS_TOO_FAST("访问频率太快",403);
/**
* 异常消息
*/
private String message;
/**
* 异常code
*/
private int code;
/**
* 枚举的构造方法,是私有的哦!初始化枚举
* @param message
* @param code
*/
private IpExcetionEnum(String message,int code){
this.message=message;
this.code=code;
}
public String getMessage() {
return message;
}
public int getCode() {
return code;
}
}5.异常处理中心—>负责异常统一处理的
注:Result是结果统一返回的对象
/**
* 创建
* 全局异常处理
* @author cc
* @date 2021-02-26-12:00
*/
@RestControllerAdvice //捕捉正常返回json的Controller异常
//@ControllerAdvice //捕捉正常Controller的异常
public class GlobalExceptionHandler {
/**
* 捕获异常
* @param e 异常类型为 IpExcetion
* @return
*/
@ExceptionHandler(value = IpExcetion.class)
public Result error(IpExcetion e) {
return Result.handelLose(e.getMessage(),e.getErrorCode());
}
}6.实际使用
/**
* 创建
* 通用访问接口
* @author cc
* @date 2021-01-31-12:32
*/
@RestController
@RequestMapping("/common")
public class CommonController {
@Autowired
private UserDataService userDataService;
@GetMapping( value = "/test/{id}",produces = MediaType.APPLICATION_JSON_VALUE)
//限制接口对于同一用户,一秒钟可以访问3次,是一个restFul风格的接口,有一个参数不需要被解析
@IpInterceptor(requestCounts =3,expiresTimeSecond = 1,isRestful = true,restfulParamCounts = 1)
public Result videoUpload(@PathVariable("id")String id) {
return Result.handelSuccess("处理成功--->"+id);
}
}7.使用效果

注
可能这个时候就有人开喷了!!!
- 要是多个用户在同一局域网呢,你怎么能拿得到用户真实的IP地址呢?
1.可以做数据埋点啊!!! 我这是微信小程序,用户都是用手机访问的!!!
2.这里一定要通过用户的IP地址吗???也可以通过用户的Token类似于访问凭证的这些东西!!!各有各的实现方式!!!但是思路都是加一层解决!
















