在一些场景下,需要防止接口被攻击或者因频繁被调用导致系统卡顿,此时限制接口的调用频率可以起到一定程度的缓和效果。

实现逻辑
定义注解,配置频率,放在需要限制调用频率的接口上。定义拦截器拦截注解,如果拦截到定义的注解,则设置redis值,key为ip+接口名称,value为调用次数,保存redis且设置保存时间为指定时间,如果值大于指定的值,拦截器不放行返回拦截信息,如果没有超过值则放行,redis的key不变,值加1。

1、定义注解
其中count为访问次数,time为指定时间

import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import java.lang.annotation.*;

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE,ElementType.METHOD})
@Inherited
@Documented
@Order(Ordered.HIGHEST_PRECEDENCE) //最高优先级
public @interface RequestLimit {
    /**
     *
     * 允许访问的次数,默认值MAX_VALUE
     */
    int count() default Integer.MAX_VALUE;

    /**
     *
     * 时间段,单位为毫秒,默认值一分钟
     */
    long time() default 60000;
}

2、使用注解
如下在控制器上添加@RequestLimit(count = 2),标识默认时间内可以调用两次接口,默认时间为1分钟,即接口1分钟仅可以被调用两次

    @RequestMapping(value = {"getDocRecvCount"}
    @RequestLimit(count = 2)
    public OpenRespModel getCount(@RequestBody(required = false) OpenModel param) {
        return this.openService.getCount(param);
    }

3、拦截器配置
拦截到指定的注解,通过设置redis的数据,key为ip加上接口,value为已经调用的次数,redis数据有效时间为定义的时间

import com.alibaba.fastjson.JSON;
import com.google.common.collect.Maps;
import com..commons.annotation.RequestLimit;
import com..commons.model.ResponseCodeEnum;
import com..util.RequestUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Map;
import java.util.concurrent.TimeUnit;

public class RequestLimitInterceptor extends HandlerInterceptorAdapter {

    private static final Logger logger = LoggerFactory.getLogger("RequestLimitInterceptor");

    @Autowired
    private RedisTemplate<String, String> redisTemplate;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //方法注解
        RequestLimit methodAnnotation = ((HandlerMethod) handler).getMethodAnnotation(RequestLimit.class);
        //类注解
        RequestLimit classAnnotation = ((HandlerMethod) handler).getBean().getClass().getAnnotation(RequestLimit.class);
        boolean vcode = true;
        if (methodAnnotation != null) {
            vcode = validateCode(request, methodAnnotation.count(), methodAnnotation.time());
        } else if (classAnnotation != null) {
            vcode = validateCode(request, classAnnotation.count(), classAnnotation.time());
        }
        if (vcode) {
            return true;
        } else {
            Map<String, Object> resultMap = Maps.newHashMap();
            resultMap.put("retCode", ResponseCodeEnum.REQUESTFULL.getRetCode());
            resultMap.put("retDesc", ResponseCodeEnum.REQUESTFULL.getRetDesc());
            try {
                response.setCharacterEncoding("UTF-8");
                response.setContentType("application/json;charset=UTF-8");
                PrintWriter pw = response.getWriter();
                pw.write(JSON.toJSONString(resultMap));
                pw.flush();
                pw.close();
            } catch (IOException e) {
                logger.error("返回页面数据出错!" + e.getMessage(), e);
                throw e;
            }
            return false;
        }
    }

    /**
     * 接口的访问频次限制
     *
     * @param request
     * @return
     */
    private boolean validateCode(HttpServletRequest request, int maxSize, long timeOut) {
        boolean resultCode = true;
        try {
            String ip = RequestUtil.getRemoteAddr(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;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {

    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {

    }
}

4、拦截器添加到项目中

 <!-- 拦截提供外部接口访问次数限制-->
        <mvc:interceptor>
            <mvc:mapping path="/openApi/**" />
            <bean class="com..commons.interceptor.RequestLimitInterceptor" />
        </mvc:interceptor>

第一,二次调用结果
在这里插入图片描述
第三次调用结果
在这里插入图片描述

过一分钟后等redis设置值失效后,便可以再次访问。

学海无涯苦作舟!!!

Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐