限制用户非正常访问

简介

原.NET代码实现请见本文最下方,这里我尝试使用瀑布开发模型对问题进行逐一分析(列出了一部分问题,但是并非所有问题),这样更方便你理解这些思想及认识到系统安全的重要性。

需求分析

问题描述

非正常请求、无脑DDOS等都是设计对外服务接口时的困难点,现在已经有一些技术可以解决这些问题比如熔断,可是这些库都很重,是否可以构建一个更加轻量级的服务?

非正常请求隐患

假设一个服务器每分钟可以支持200个用户进行30次的请求,那么我的应用也就是可以支持200用户。但是假设其中包含了一个非正常用户,该用户是黑客,黑客使用手段每分钟请求6000次,那么我的应用将没有多余流量分给别的用户,服务此时就挂掉了。

熔断思想

熔断是全自动化的,熔断思想简略来说就是对某范围进行控制,当某些动作超出了这个范围,我们就可以自动地对该操作进行控制,比如限制操作等。

软件设计

存在的问题

  • 1、我们想要拦截用户请求,必须要考虑 的是如何拿到用户的代表性数据,这样才能对用户做出控制操作
  • 2、我们需要容器来存储这些信息,应用程序处理请求是多线程的,所以我们必须考虑多线程及多线程安全问题
  • 3、我们需要一个容器来存储这些数据,该容器应该是单例的,只有单例时,我们才能控制所有的用户请求,将被暂时拉黑请求的用户存入容器,这也就是黑名单策略

代表性数据

我们需要拿到能代表用户的唯一数据,考虑使用以下数据:

  • 用户IP: 当使用NAT协议时,多用户共享同一IP
  • 使用非对称令牌: 服务器签发令牌,使用Cookie存储
  • URI: 对整个URI进行限制,缓解服务器具体资源压力

Let's Coding(编码)

容器相关问题

  • 容器应该是一个查找表,一般的查找表包括:二叉平衡树、哈希表等,这里使用场景是黑名单,所以哈希表相对更合适

  • 容器必须且应该是是单例的,也就是我们的应用程序只有一份黑名单,这样才能更好地让我们去控制该容器,从而控制黑名单中的用户,使用static关键字可以将应用程序中的资源变为单例的,

  • 容器安全其实也就是线程的安全性,可以使用一般的锁或者synchronized。因为不想让具体业务代码繁杂和冗余,使用synchronized代码块对基本操作进行封装,并将每个基本操作封装为独立的方法

  • Synchronized到底锁谁?我不想将容器给锁起来,这样可能会存在一些问题,这样的话用户做操作的时候,我将失去容器的控制权。最后决定使用Java中的一种锁方法,对private static final Object对象上锁(具体上锁的对象是单例且对外不可见)

服务应用

  • 限制请求应该在我们还没有进行到某些业务时就要生效,因为假设用户已经访问到具体业务我们再进行限制,这样的含义表示我们只是不想给这些用户对应的资源而已,但其实这个时候资源已经被浪费了。

  • 如何针对每个接口都进行不同次数的拦截?在具体Controller上使用注解是一个很好的方式

  • 注解声明


@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MinutesRequestLimit {
    int limit() default 10;
}

  • Spring MVC中的拦截器可以读取到Controller中的注解(我不知道过滤器可不可以这样做,我只在拦截器中这样做过......)

  • 将组件交给Spring容器托管


@Component
public class RequestLimitInterceptor implements HandlerInterceptor {
    // container
    public static Map<String, Integer> MemoryMap = new HashMap<String, Integer>();
    //Gets the last time the container was clear
    public static long LastClearTime = new Date().getTime();    
    //Objects to lock
    private static final Object _obj = new Object();

    // limit time,This value needs to be passed by the developer
    private int limit = 0;  

    // container Write operation
    public static void WriteMemory(String key, Integer value)
    {
        synchronized (_obj)
        {
            MemoryMap.put(key, value);
        }
    }

    // container clear operation
    public static void ClearMemory()
    {
        synchronized (_obj)
        {
            MemoryMap.clear();
        }
    }

    // container copy operation
    public static HashMap<String,Integer> Copy()
    {
        synchronized (_obj)
        {
            return new HashMap<String, Integer>(MemoryMap);
        }
    }
}

其它相关问题

  • 限制次数的初始化(读取到注解中具体的限制次数),这个空值判断是因为Spring MVC会对拦截器做一次回调,回调时handler是空值的,所以回调时不执行初始化操作,这个判空操作放在preHandle中也是可以的

public void Init(Object handler)
{
    if(handler instanceof HandlerMethod)
    {
        HandlerMethod handlerMethod = (HandlerMethod) handler;
        Method method = handlerMethod.getMethod();
        MinutesRequestLimit annotation = method.getAnnotation(MinutesRequestLimit.class);
        if(annotation != null)
            limit = annotation.limit();
    }
}

  • 使用用户的IP+访问URI方式对用户请求进行熔断:我们设计这个的本质是缓解服务器压力,所以没有必要进行特别精准的控制

  • 真的需要精确限制每个用户某一段时间的具体访问次数吗?其实是不需要的,因为我们的本质是想要缓解服务器压力而已,精确控制其实也会浪费一定的资源

  • 当因客户端某些错误请求导致无法进行正常数据返回时,返回4系列状态码,HTTP协议中429状态码(Too Many Requests)正是此处所需要的

public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {

    //Init a minutes limit request parameter
    Init(handler);

    // Is the calculation time greater than one minute
    if((new Date().getTime() - LastClearTime) / 1000 > 60)
    {
        ClearMemory();
        LastClearTime = new Date().getTime();
    }

    // use temp container Indirect operation container
    HashMap<String,Integer> temp = Copy();

    String path = request.getRequestURI();
    String ip = request.getRemoteAddr();

    if(temp.containsKey(ip + path))
    {
        WriteMemory(ip + path,temp.get(ip + path) + 1);
        temp = Copy();
        if(temp.get(ip + path) > limit)
        {
            // If the user is restricted, set header info and 429 status code
            response.setHeader("second-retry-after",
                    String.valueOf(60 - (new Date().getTime() - LastClearTime) / 1000));
            response.setStatus(HttpStatus.TOO_MANY_REQUESTS.value());
            // False means the request here is incorrect
            return false;
        }
    }else
    {
        temp.put(ip + path, 1);
        WriteMemory(ip + path, 1);
    }
    return true;
}
    

拦截器注册

  • 拦截除了指定静态资源以外的所有资源

  • 从Spring容器中取出拦截器实例


@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

    @Autowired
    private RequestLimitInterceptor requestLimitInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {

        registry.addInterceptor(requestLimitInterceptor)
                .excludePathPatterns("/*/*.css", "/*/*.js", "/*/*.png", "/*/*.jpg", "/*/*.jpeg");
    }
}

测试代码

测试接口


    @MinutesRequestLimit(limit = 5)
    @RequestMapping(path = "/index", method = RequestMethod.GET)
    public String getHome() {

        return "index";
    }

测试结果

test result

简要说明

  • 本代码原作者为Anduin,其代码实现平台为.NET,后我根据其逻辑改写为Java相关平台运行
  • 原博文地址:Anduin Xue
  • 原代码地址:Github for Anduin 2017
posted @ 2021-01-11 11:48  Erosion2020  阅读(225)  评论(0)    收藏  举报