《花100块做个摸鱼小网站! 》第九篇—我的小网站被攻击了!

⭐️基础链接导航⭐️

服务器 → ☁️ 阿里云活动地址

看样例 → 🐟 摸鱼小网站地址

学代码 → 💻 源码库地址

一、前言

大家好呀,我是summo,最近不是被裁员了嘛,找工作又难找,老是心烦意乱的,也导致了一个多月断更,不好意思哈。虽然工作没了,但是小网站的故事却一直在发生,比如阿里云RDS到期导致我的小网站崩了一个月(已经修复);比如我重构了一下设计和代码(代码已上传Gitee);比如有些“好心人”刷我的接口,给我提高访问量(本篇会讲的故事)... ...

还有一些更有意思的,比如济南有位老板看中了我的小网站希望我可以根据他的一些需求进行二次开发(一期已经做好上线了);比如通过这个小网站让我赚到1千多的外快(意外之喜)等等。

工作是一时的,兴趣是一辈子的,无论如何,从今天开始,我会继续更新小网站的故事,这一篇主题:我的小网站被攻击了!

二、被攻击的过程

在我小网站的右侧有一块访问统计的展示,显示着今日PV,今天UV、总PV和总UV(PV:页面浏览量,访问一次我加一次;UV:独立访客数,一个IP我算一个),如下图:

这是2024-11-22截的图,总UV才2W多,但是PV已经8W多了,相当于每个用户平均访问了小网站4次,虽然看起来也不算很离谱,但实际上有几个“好心人”单独就贡献了1.5W次的访问,如下图: 至于他们为什么攻击我,跟我也是有很大关系,我曾经在博客园的闪存“大放厥词”:

三、小网站的统计规则

之前我们在第七篇—谁访问了我们的网站?中介绍了小网站的统计逻辑,核心逻辑就是加了一个@VisitLog注解,使用切面的方式记录访问的IP地址。然后将这个注解放在了IndexController.java的index方法上,如下:

1@Controller 2public class IndexController { 3 4 @GetMapping("/") 5 @VisitLog 6 public String index(HttpServletRequest request) { 7 if (isFromMobile(request)) { 8 return "mp/index"; 9 } else { 10 return "web/index"; 11 } 12 } 13}

也就是说,只要你们疯狂调用https://sbmy.fun,就会不断地增加访问记录。正如我前面所说的我并没有使用CDN、OSS这种按按流量计费的组件,无论你们怎么刷都是不会给我造成资损的,但是有一个问题我不得不处理一下: 前端的资源来自于我的应用,资源包括js、css、图片、字体等文件,这些资源还都不小,尤其是chunk-vendors.js,体积达到了1.2M,如下图: 阿里云的ECS公网IP带宽默认只有3M,也就是说小网站同时有三个人访问就会出现访问效率问题,问题其实也不大,浏览器只要访问一次这些资源就会将它们缓存下来,后续再刷新也不会很慢。但是如果被攻击了呢,那就麻烦了,正常用户想访问都不行了... ... 所以我必须得想个法子了。

四、限流大法好

像我们这种小网站,流量也不大,被攻击了好像也没啥意义,有些攻击纯粹就是兄弟们开的玩笑,搞着玩的。但既然兄弟们出招了,我这边也得想个法子解决不是?其实很简单,搞个滑动窗口限流就可以了。

我先讲一下什么是限流,王者荣耀大家都玩过吧,里面的英雄都有一个攻击间隔,当我们连续的点击普通攻击的时候,英雄的攻速并不会随着我们点击的越快而更快的攻击。这个就是限流,英雄会按照自身攻速的系数执行攻击,我们点的再快也没用。

1、滑动窗口限流

先上一张流程图,帮助大家理解原理

2、原理说明

从图上可以看到时间创建是一种滑动的方式前进, 滑动窗口限流策略能够显著减少临界问题的影响,但并不能完全消除它。滑动窗口通过跟踪和限制在一个连续的时间窗口内的请求来工作。与简单的计数器方法不同,它不是在窗口结束时突然重置计数器,而是根据时间的推移逐渐地移除窗口中的旧请求,添加新的请求。 举个例子:假设时间窗口为10s,请求限制为3,第一次请求在10:00:00发起,第二次在10:00:05发起,第三次10:00:11发起,那么计数器策略的下一个窗口开始时间是10:00:11,而滑动窗口是10:00:05。所以这也是滑动窗口为什么可以减少临界问题的影响,但并不能完全消除它的原因。

如果大家想详细了解一些常用的限流算法,可以看我这篇文章:《优化接口设计的思路》系列:第七篇—接口限流策略

3、代码实现

SlidingWindowRateLimit.java

1package com.summo.demo.config.limitstrategy.slidingwindow; 2 3import java.lang.annotation.ElementType; 4import java.lang.annotation.Retention; 5import java.lang.annotation.RetentionPolicy; 6import java.lang.annotation.Target; 7 8@Retention(RetentionPolicy.RUNTIME) 9@Target(ElementType.METHOD) 10public @interface SlidingWindowRateLimit { 11 /** 12 * 请求的数量 13 * 14 * @return 15 */ 16 int requests(); 17 18 /** 19 * 时间窗口,单位为秒 20 * 21 * @return 22 */ 23 int timeWindow(); 24}

SlidingWindowRateLimitAspect.java

1package com.summo.sbmy.common.limit.slidingwindow; 2 3import com.summo.sbmy.common.util.HttpContextUtil; 4import com.summo.sbmy.common.util.IpUtil; 5import lombok.extern.slf4j.Slf4j; 6import org.aspectj.lang.ProceedingJoinPoint; 7import org.aspectj.lang.annotation.Around; 8import org.aspectj.lang.annotation.Aspect; 9import org.aspectj.lang.reflect.MethodSignature; 10import org.springframework.core.annotation.Order; 11import org.springframework.stereotype.Component; 12 13import java.lang.reflect.Method; 14import java.util.concurrent.ConcurrentHashMap; 15import java.util.concurrent.ConcurrentLinkedQueue; 16import java.util.concurrent.TimeUnit; 17 18@Slf4j 19@Aspect 20@Component 21@Order(5) 22public class SlidingWindowRateLimitAspect { 23 /** 24 * 使用 ConcurrentHashMap 保存每个方法的请求时间戳队列 25 */ 26 private final ConcurrentHashMap<String, ConcurrentHashMap<String, ConcurrentLinkedQueue<Long>>> METHOD_IP_REQUEST_TIMES_MAP = new ConcurrentHashMap<>(); 27 28 @Around("@annotation(com.summo.sbmy.common.limit.slidingwindow.SlidingWindowRateLimit)") 29 public Object rateLimit(ProceedingJoinPoint joinPoint) throws Throwable { 30 MethodSignature signature = (MethodSignature) joinPoint.getSignature(); 31 Method method = signature.getMethod(); 32 // 获取客户端IP地址 33 String clientIp = IpUtil.getIpAddr(HttpContextUtil.getHttpServletRequest()); 34 35 36 SlidingWindowRateLimit rateLimit = method.getAnnotation(SlidingWindowRateLimit.class); 37 // 允许的最大请求数 38 int requests = rateLimit.requests(); 39 // 滑动窗口的大小(秒) 40 int timeWindow = rateLimit.timeWindow(); 41 42 // 获取方法名称字符串 43 String methodName = method.toString(); 44 // 如果不存在当前方法和IP的请求时间戳队列,则初始化一个新的队列 45 ConcurrentHashMap<String, ConcurrentLinkedQueue<Long>> ipRequestTimesMap = METHOD_IP_REQUEST_TIMES_MAP.computeIfAbsent(methodName, k -> new ConcurrentHashMap<>()); 46 ConcurrentLinkedQueue<Long> requestTimes = ipRequestTimesMap.computeIfAbsent(clientIp, k -> new ConcurrentLinkedQueue<>()); 47 48 // 当前时间 49 long currentTime = System.currentTimeMillis(); 50 // 计算时间窗口的开始时间戳 51 long thresholdTime = currentTime - TimeUnit.SECONDS.toMillis(timeWindow); 52 53 // 这一段代码是滑动窗口限流算法中的关键部分,其功能是移除当前滑动窗口之前的请求时间戳。这样做是为了确保窗口内只保留最近时间段内的请求记录。 54 // requestTimes.isEmpty() 是检查队列是否为空的条件。如果队列为空,则意味着没有任何请求记录,不需要进行移除操作。 55 // requestTimes.peek() < thresholdTime 是检查队列头部的时间戳是否早于滑动窗口的开始时间。如果是,说明这个时间戳已经不在当前的时间窗口内,应当被移除。 56 while (!requestTimes.isEmpty() && requestTimes.peek() < thresholdTime) { 57 // 移除队列头部的过期时间戳 58 requestTimes.poll(); 59 } 60 61 // 检查当前时间窗口内的请求次数是否超过限制 62 if (requestTimes.size() < requests) { 63 // 未超过限制,记录当前请求时间 64 requestTimes.add(currentTime); 65 return joinPoint.proceed(); 66 } else { 67 // 超过限制,抛出限流异常 68 throw new RuntimeException("Too many requests, please try again later."); 69 } 70 } 71}

使用方式

1 2@Controller 3public class IndexController { 4 @GetMapping("/") 5 @VisitLog 6 @SlidingWindowRateLimit(requests = 2, timeWindow = 2) 7 public String index(HttpServletRequest request) { 8 if (isFromMobile(request)) { 9 return "mp/index"; 10 } else { 11 return "web/index"; 12 } 13 } 14}

五、小结一下

上面的限流使用的是ConcurrentHashMap来存储每个方法的请求时间戳队列,适用于单机,如果是分布式的环境则可以换成Redis。通过在资源接口上加一个限流的方式我们可以防止单个IP刷爆我们的index接口,防止带宽打满,我试了下应该是用的,就是不知道实战如何。

点赞
收藏

评论区

加载中...

相关推荐

有了它,全球网络摄像头一览无余

大家好,我是IT共享者,人称皮皮。前言相信大家对于以前的网络摄像头泄露,各大宾馆开房视频频繁泄露,一定不会陌生了吧,当时,小编也在想,这些黑客是如何办到的了,本期小编就来为大家进行解密,揭开这层神秘的面纱。一、网站获取1.ZoomEy中文名叫钟馗之眼,是专门用来获取全球网络摄像头的网站解析库,界面很美而且简洁,如图:我们可以通过输入关键词来搜索相关

我的网站被ddos攻击了怎么办?

像题主这样的情况,可以先等攻击的情况缓解了以后,再去优化并且建立自己网站的防御系统。不管是什么网站,遭受到一次DDoS攻击产生的后果还是很严重的。而且基本上每过一段时间,新闻就会报道某大型网站遭到DDoS攻击,导致网站瘫痪,无法正常访问的内容。可见DDoS攻击危害之大。今天队长就带大家看看DDoS攻击时我们应该怎么办?怎么做好防御工作?在这之前我们先系统地了

我有点想用JDK17了

大家好呀,我是summo,JDK版本升级的非常快,现在已经到JDK20了。JDK版本虽多,但应用最广泛的还得是JDK8,正所谓“他发任他发,我用Java8”。其实我也不太想升级JDK版本,感觉投入高,收益小,不过有一次我看到了一些使用JDK17新语法写的代

《花100块做个摸鱼小网站! · 序》灵感来源

大家好呀,我是summo,这次来写写我在上班空闲(摸鱼)的时候做的一个小网站的事。去年阿里云不是推出了个活动嘛,2核2G的云服务器一年只要99块钱,懂行的人应该知道这个价格在业界已经是非常良心了,虽然优惠只有一年,但是买一台用来学习还是非常合适的。

《花100块做个摸鱼小网站! 》第一篇—买云服务器和初始化环境

大家好呀,我是summo,前面我已经写了我为啥要做这个摸鱼小网站的原因,从这篇文章开始我会一步步跟大家聊聊我是怎么搭起这个网站的。我知道对很多新手来说,建网站可能挺头大的,不知道从哪里开始,所以我会尽量写得简单明了,让大家一看就懂,少走弯路。

《花100块做个摸鱼小网站! 》第二篇—后端应用搭建和完成第一个爬虫

大家好呀,我是summo,前面已经教会大家怎么去阿里云买服务器(链接在这,需要自取),以及怎么搭建JDK、Redis、MySQL这些环境。从这篇文章开始就进入正式的编码阶段了,我们从后端开始,先把热搜数据获取到,然后再开始前端部分。