-
API 接口防刷
顾名思义,想让某个接口某个人在某段时间内只能请求N次。 在项目中比较常见的问题也有,那就是连点按钮导致请求多次,以前在web端有表单重复提交,可以通过token 来解决。 除了上面的方法外,前后端配合的方法。现在全部由后端来控制。
-
原理
在你请求的时候,服务器通过redis 记录下你请求的次数,如果次数超过限制就不给访问。 在redis 保存的key 是有时效性的,过期就会删除。
-
代码实现:
为了让它看起来逼格高一点,所以以自定义注解的方式实现
①自定义注解
import java.lang.annotation.*;
/**
* 请求限制的自定义注解
* /
@Documented
@Inherited
@Target({ElementType.METHOD,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface RequestLimit {
// 在 second 秒内,最大只能请求 maxCount 次
int second() default 1;
int maxCount() default 1;
}
②自定义拦截器
package com.demo.antiBrushing;
import com.alibaba.fastjson.JSONObject;
import com.demo.Result.ChResult;
import lombok.extern.slf4j.Slf4j;
import org.jetbrains.annotations.NotNull;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.lang.reflect.Method;
import java.util.concurrent.TimeUnit;
/**
* 请求拦截
*/
@Slf4j
@Component
public class RequestLimitIntercept extends HandlerInterceptorAdapter {
@Resource
private RedisTemplate<String,Object> redisTemplate;
@Override
public boolean preHandle(@NotNull HttpServletRequest request, @NotNull HttpServletResponse response, Object handler) throws Exception {
/*
isAssignableFrom() 判定此 Class 对象所表示的类或接口与指定的 Class 参数所表示的类或接口是否相同,或是否是其超类或超接口
isAssignableFrom()方法是判断是否为某个类的父类
instanceof关键字是判断是否某个类的子类
*/
if(handler.getClass().isAssignableFrom(HandlerMethod.class)){
//HandlerMethod 封装方法定义相关的信息,如类,方法,参数等
HandlerMethod handlerMethod = (HandlerMethod) handler;
Method method = handlerMethod.getMethod();
// 获取方法中是否包含注解
RequestLimit methodAnnotation = method.getAnnotation(RequestLimit.class);
//获取 类中是否包含注解,也就是controller 是否有注解
RequestLimit classAnnotation = method.getDeclaringClass().getAnnotation(RequestLimit.class);
// 如果 方法上有注解就优先选择方法上的参数,否则类上的参数
RequestLimit requestLimit = methodAnnotation != null?methodAnnotation:classAnnotation;
if(requestLimit != null){
if(isLimit(request,requestLimit)){
resonseOut(response, ChResult.error().setMsg("请求太快了,请稍后刷新"));
return false;
}
}
}
return super.preHandle(request, response, handler);
}
//判断请求是否受限
public boolean isLimit(HttpServletRequest request,RequestLimit requestLimit){
// 受限的redis 缓存key ,因为这里用浏览器做测试,我就用sessionid 来做唯一key,如果是app ,可以使用 用户ID 之类的唯一标识。
String limitKey = request.getServletPath()+request.getSession().getId();
// 从缓存中获取,当前这个请求访问了几次
Integer redisCount = (Integer) redisTemplate.opsForValue().get(limitKey);
if(redisCount == null){
//初始 次数
redisTemplate.opsForValue().set(limitKey,1,requestLimit.second(), TimeUnit.SECONDS);
}else{
if(redisCount >= requestLimit.maxCount()){
return true;
}
// 次数自增
redisTemplate.opsForValue().increment(limitKey);
}
return false;
}
/**
* 回写给客户端
* @param response
* @param result
* @throws IOException
*/
private void resonseOut(HttpServletResponse response, ChResult result) throws IOException {
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json; charset=utf-8");
PrintWriter out = null ;
String json = JSONObject.toJSON(result).toString();
out = response.getWriter();
out.append(json);
}
}
拦截器写好了,但是还得添加注册
WebMvcConfig 配置类 因为我的是Springboot2.* 所以只需实现WebMvcConfigurer 如果是springboot1.* 那就继承自 WebMvcConfigurerAdapter 然后重写addInterceptors() 添加自定义拦截器即可。
@Slf4j
@Component
public class WebMvcConfig implements WebMvcConfigurer {
@Autowired
private RequestLimitIntercept requestLimitIntercept;
@Override
public void addInterceptors(InterceptorRegistry registry) {
log.info("添加拦截");
registry.addInterceptor(requestLimitIntercept);
}
}
控制层测试接口,
使用方式:
第一种:直接在类上使用注解@RequestLimit(maxCount = 5,second = 1) 第二种:在方法上使用注解@RequestLimit(maxCount = 5,second = 1)
可以同时加在方法和类上