限制用户非正常访问
简介
原.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";
}
测试结果
简要说明
- 本代码原作者为Anduin,其代码实现平台为.NET,后我根据其逻辑改写为Java相关平台运行
- 原博文地址:Anduin Xue
- 原代码地址:Github for Anduin 2017