接口防刷

  • 背景介绍

目前大部分公司都采用前后端分离的开发方式,进行项目的并行开发。在项目中后台只需要提供一套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());
    }
}

  • 后记

本文只是提供接口防刷的一个简单的解决方案,还有许多其他的实现方式,在许多大型互联网公司,接口防刷技术更加复杂,但是接口防刷的原理基本相似。

你可能感兴趣的:(spring,redis)