- 背景介绍
目前大部分公司都采用前后端分离的开发方式,进行项目的并行开发。在项目中后台只需要提供一套API接口,就可以接入安卓、小程序、IOS、web等多个应用程序,这样可以节约开发成本。但是一个后台接入这么多应用程序的http请求,必然导致后端的压力非常大。所以对于一些请求进行过滤和拦截是非常有必要的,能够有效地减轻后台的压力。
接口防刷机制:主要防止短时间接口被大量调用(攻击),出现系统崩溃和系统爬虫问题,提升服务的可用性。
本文主要是通过 注解+redis+spring aop+全局异常的方式实现接口防刷功能。
- 防刷注解
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface RequestLimit {
int maxCount() default 5;
//建议时间长一点,redis对于1s失效时间会出现无法过期的情况
int second() default 1;
//默认为五分钟
long expireTime() default 5*60;
//提示信息
String message() default "短时间内访问次数超出限制";
}
- spring AOP切面
@Component
@Aspect
public class RequestAspect {
private static final Logger logger = Logger.getLogger(RequestAspect.class);
@Resource
private RedisTemplate redisTemplate;
@Pointcut("@annotation(com.shu.redis.antibrush.aspect.RequestLimit)")
private void authAccess() {
}
//这里写的为环绕触发,可自行根据业务场景选择@Before @After
//触发条件为:(edu.whut.pocket.*.controller包下面所有类且)含有注解@RequestLimit
@Before(value = "authAccess() && @annotation(requestLimitAnnotation) ",argNames = "joinPoint,requestLimitAnnotation")
public void doBeforeMethod(JoinPoint joinPoint, RequestLimit requestLimitAnnotation) throws Exception {
//请求参数名-值
Signature signature = joinPoint.getSignature();
MethodSignature methodSignature = (MethodSignature) signature;
String[] paramsKey = methodSignature.getParameterNames();
Object[] paramsValue = joinPoint.getArgs();
String message=requestLimitAnnotation.message();
int maxCount = requestLimitAnnotation.maxCount();
int second = requestLimitAnnotation.second();
// long expireTime = requestLimitAnnotation.expireTime();
//正是上线可以调整
long expireTime = 2;
Object result = null;
//对ip做校验
//request对象
HttpServletRequest request = (HttpServletRequest) paramsValue[0];
String ip = IPUtil.getIpAddress(request);
String requestURI = request.getRequestURI();
Object object = redisTemplate.opsForValue().get("riskIp:" + ip);
if (object!=null){
Boolean hasKey = redisTemplate.hasKey("requestLimit:" + requestURI + "-" + ip);
if (hasKey==true){
//防止出现过期时间为-1的情况,强制删除
redisTemplate.delete("requestLimit:" + requestURI + "-" + ip);
}
//用于提示信息
throw new RequestLimitException(message);
}
if (object==null) {
//利用redis的存活时间自动对request的请求进行检验
Integer requestCount = (Integer) redisTemplate.opsForValue().get("requestLimit:"+requestURI+"-" + ip);
if (requestCount == null||requestCount==0) {
logger.info("重新加入redisIp:"+ip);
redisTemplate.opsForValue().set("requestLimit:"+requestURI+"-" + ip, 1, second, TimeUnit.SECONDS);
return;
}else {
if (requestCount >= maxCount) {
//请求的时间超时,将这个ip关进小黑屋
long timeMillis = System.currentTimeMillis();
redisTemplate.opsForValue().set("riskIp:" + ip, timeMillis, expireTime, TimeUnit.SECONDS);
throw new RequestLimitException(message);
}else {
//开始放行
redisTemplate.opsForValue().increment("requestLimit:"+requestURI+"-" + ip, 1);
logger.info("放行ip:"+ip);
Long expire = redisTemplate.getExpire("requestLimit:" + requestURI + "-" + ip);
logger.info("millexpire:"+expire);
}
}
}
}
}
- 异常处理机制
public class RequestLimitException extends RuntimeException {
public RequestLimitException(String message) {
super(message);
}
}
- @ControllerAdvice
@ResponseBody
public class WebExceptionHandler {
private static Logger logger = Logger.getLogger(WebExceptionHandler.class);
//全局异常处理
@ExceptionHandler(RequestLimitException.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public Map handleRequestException(RequestLimitException e){
ResponseMap map = ResponseMap.getInstance();
return map.putFailure(e.getMessage(), HttpStatus.INTERNAL_SERVER_ERROR.value());
}
}
- 后记
本文只是提供接口防刷的一个简单的解决方案,还有许多其他的实现方式,在许多大型互联网公司,接口防刷技术更加复杂,但是接口防刷的原理基本相似。