springcloud-zuul 网关 控制同ip同接口访问频率

上周末公司线上环境出问题了,某个服务容器老是崩溃。忧心忡忡的排查一番后,发现是有个接口在被爬虫轮询扒数据,之前业务原因这个接口没有身份验证,临时加上了,也查到对方几个ip封掉。公司开会计划年后前后端加入严格点的加密验证,年前让我暂时在后端做一下优化(前端有app,让客户年前更新成本太大)。
思路是这样的:对于所有接口 同一个请求地址,并且同一个ip, 5秒内最多请求5次。超过的话提示访问过于频繁,5秒无请求后恢复,有请求重新延长5秒。
其实google的Guava包 令牌桶RateLimiter类 已经很好了,不过只适合单应用模式,需要修改。就没有使用它了。下面是自己通过redis保存时间戳的方式 简单的控制了请求频率:

/**
 * ip接口访问记录对象
 * @Auther: Administrator
 * @Date: 2019/1/30 15:50
 * @Description:
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
public class AccessTimeOut {
    //同ip同接口首次请求的时间戳
    private long  timestamp;
    //时间段内的访问次数
    private int count;
}

需要缓存的对象,本人最近刚用上的lombok包,代码简洁不少。

/**
 * 缓存接口访问信息
 * @Auther: Administrator
 * @Date: 2019/1/30 10:59
 * @Description:
 */
@Service
public class RedisService {
    @Cacheable(value = "accesscount", key = "'accesscount_'+#key")
    public AccessTimeOut get(String key){
        AccessTimeOut timeOut = new AccessTimeOut(DateUtils.nowTimeMillis(),0);
        return timeOut;
    }
    @CachePut(value = "accesscount", key = "'accesscount_'+#key")
    public AccessTimeOut update(String key,AccessTimeOut timeOut){
        timeOut.setCount(timeOut.getCount()+1);
        return timeOut;
    }
    @CacheEvict(value = "accesscount", key = "'accesscount_'+#key")
    public void del(String key) {
    }
}

这是缓存操作,这网关应用只有此处用到redis,所以名字起得随意。
DateUtils.nowTimeMillis()方法就是封装的System.currentTimeMillis()
然后以下所有代码都是网关的拦截器类里面,隐去了不相关的业务代码

@Component
public class AccessFilter extends ZuulFilter {
//时间段内(缓存有效期)同一IP对相同接口的最大访问次数
    final static int MAX_ACCESS_COUNT = 5;
    final static int MAX_ACCESS_TIMEOUT = 5000;

    final static org.slf4j.Logger logger = org.slf4j.LoggerFactory.getLogger(AccessFilter.class);

    @Autowired
    RedisService redisService;

    @Override
    public String filterType() {
        return FilterConstants.PRE_TYPE;
    }

    @Override
    public int filterOrder() {
        return FORM_BODY_WRAPPER_FILTER_ORDER - 1;
    }

    @Override
    public boolean shouldFilter() {
        return true;
    }

    @Override
    public Object run() {
    RequestContext ctx = RequestContext.getCurrentContext();
        HttpServletRequest request = ctx.getRequest();

        String token = request.getHeader("token");
        if (token == null) {
            token = request.getParameter("token");
        }
        if(accessIsOut(request.getServletPath(),CommonUtil.getIpAddr(request),token)){
            //判断ip是否超过接口访问频率
            logger.warn("访问超过次数!");
            HttpServletResponse response = ctx.getResponse();
            response.setCharacterEncoding("utf-8");  //设置字符集
            response.setContentType("text/html; charset=utf-8"); //设置相应格式
            response.setStatus(200);
            ctx.setSendZuulResponse(false); //不进行路由
            try {
                response.getWriter().print("{\"code\":\"416\",\"message\":\"短时间内请求过多!\"}"); //响应体
            } catch (IOException e) {
                e.printStackTrace();
            }
            ctx.setResponse(response);
            return null;
        }
        ······略去其他无关代码
        ctx.setSendZuulResponse(true);// 对该请求进行路由
        return null;
    }
        private boolean accessIsOut(String address,String ip,String token){
        logger.info(String.format("请求:[%s][%s][%s]",address,ip,token));
        //去掉最后一个/后面内容,以去掉大部分get请求参数
        String key = (address.length()<30?address:address.substring(0,address.lastIndexOf("/")))+"_"+ip;
        AccessTimeOut oldTimeOut  = redisService.get(key);
        AccessTimeOut timeOut = redisService.update(key,oldTimeOut);
        if(timeOut.getCount()>MAX_ACCESS_COUNT){
            if(DateUtils.nowTimeMillis() - timeOut.getTimestamp()<MAX_ACCESS_TIMEOUT) {
                //从第一次计数到超出,是否还在5秒内
                timeOut.setTimestamp(DateUtils.nowTimeMillis());
                redisService.update(key,timeOut);
                return true;
            }
            redisService.del(key);
        }
        return false;
    }
}

另外上面获取ip的方法如下,一些请求是代理转发过来的可能需要这样才能拿到真实ip:

public static String getIpAddr(HttpServletRequest request){

        String ip = request.getHeader("X-Forwarded-For");
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("Proxy-Client-IP");
        }else {
            if (ip.indexOf(",") > 0) {
                ip = ip.split(",")[0];
            }
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("WL-Proxy-Client-IP");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("HTTP_CLIENT_IP");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("HTTP_X_FORWARDED_FOR");
        }
        if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getRemoteAddr();
        }
        return ip;
    }

以上就是所有内容,已经上线测试环境一天了对业务没什么影响,只是功能有点弱鸡。。
年前和过年这段时间还是以人为监控+封ip为主吧,应该问题不大。
补充:今天被老大给否了,说生产环境记好日志就行

你可能感兴趣的:(springboot)