实战解析:接口限流的一次简单实践

1.写这篇文章的来由

有一段时间里,博客总是三天两头被打,其中就遇到了恶意刷接口的手段,对方明显使用的代码IP,由于博客并没有做这方面的措施,加上被大量盗刷的接口刚好是数据量最大的一篇文章数据,所以不出意外的,博客没多久就崩了。服务器状态也是各种异常。所以吃一堑长一智吧算是,我也没想到面对一个个人小破站,对面也是饥不择食….真大黑客啊兄弟们!!!

请在此添加图片描述


2.接口限流的常见手段

​ 现在来说,做限流的各种方案其实已经相对很成熟了,这里也是大致列举了几种常用的解决方案,但不会全部都细说。

毕竟很多都还是自己没有实际使用过的,光搞理论是没什么意义的,所以后续有时间打算一个个揪出来细搞,起码得到用过了再写篇文章总结一下吧。

Java中常用的限流解决方案:

  • 计数器
  • 滑动窗口
  • 漏桶
  • 令牌桶
  • Redis+Lua分布式限流

由于我博客采用的就是计数器方案,所以这里主要记录一下整个大致的限流原理以及实践过程。

上面几种方案中,计数器算是最简单的限流算法了。原理就是在指定的时间间隔内,对接口的请求次数进行限制,具体到我的博客为例,我是针对每个IP进行的请求限制,对请求进行计数,判断请求数量与阈值的情况,决定是否需要限流,每个IP触发限流之后会有一定的时间周期,计数器到时清零即可。

这就是计数器限流基本的原理。具体的实现上,我选用了Redis作为了计数限流的中间件,所以也可以理解为,这是Redis+计数器的一种实现方式。具体执行的逻辑如下:

  • 设置好计数器count,每过一次请求计数器就+1,同时记录对应的请求IP
  • 当下一个请求到来之际,首先通过IP判断对应的计数器是否达到了限流的频次,以及本次请求是否还在设定的请求周期内;
  • 如果请求已触发限流阈值,则针对该IP开启限流,后面的所有请求均直接拒绝。
  • 当被限流IP达到时间周期满之后,将count重置,计数器进入下一轮的就绪状态。

原理也是蛮简单的,我也是蛮喜欢这种方式的(床言床语???)。下面开始具体的实操部分。


3.计数器限流实践

首先确定实现的具体方案,上面说了,我这里用的是Redis作为限流计数器的记录以及限流状态的重置等操作。具体限流的逻辑直接写以Java带代码写在了项目业务中。

特别的,由于是通过IP来限流的,所以这里需要用大的几个处理IP地址的工具类就先贴出来。

个人习惯,我贴代码会将所有import的包都一起贴进来,这样是方便后续回顾或者学习的时候处理一些包的问题,之前就遇到过很多类似的问题(可能对小白不太友好),代码是有了,结果在导包的时候要么是对用到的哪些包不明所以,要么是同名的包过多,不知道怎么选择。

3.1 IP工具类

import eu.bitwalker.useragentutils.UserAgent;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.lionsoul.ip2region.DataBlock;
import org.lionsoul.ip2region.DbConfig;
import org.lionsoul.ip2region.DbSearcher;
import org.lionsoul.ip2region.Util;
import org.springframework.core.io.ClassPathResource;
import org.springframework.stereotype.Component;
import org.springframework.util.FileCopyUtils;
import javax.annotation.PostConstruct;
import javax.servlet.http.HttpServletRequest;
import java.io.InputStream;
import java.lang.reflect.Method;
import java.net.InetAddress;
import java.net.UnknownHostException;


@Slf4j
@Component
public class IpUtils {
    
    
    /**
     * 获取ip地址
     */
    public static String getIpAddress(HttpServletRequest request) {
        String ipAddress = request.getHeader("X-Real-IP");
        if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
            ipAddress = request.getHeader("x-forwarded-for");
        }
        if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
            ipAddress = request.getHeader("Proxy-Client-IP");
        }
        if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
            ipAddress = request.getHeader("WL-Proxy-Client-IP");
        }
        if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
            ipAddress = request.getHeader("HTTP_CLIENT_IP");
        }
        if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
            ipAddress = request.getHeader("HTTP_X_FORWARDED_FOR");
        }
        if (ipAddress == null || ipAddress.length() == 0 || "unknown".equalsIgnoreCase(ipAddress)) {
            ipAddress = request.getRemoteAddr();
            if ("127.0.0.1".equals(ipAddress) || "0:0:0:0:0:0:0:1".equals(ipAddress)) {
                //根据网卡取本机配置的IP
                InetAddress inet = null;
                try {
                    inet = InetAddress.getLocalHost();
                } catch (UnknownHostException e) {
                    log.error("getIpAddress exception:", e);
                }
                assert inet != null;
                ipAddress = inet.getHostAddress();
            }
        }
        return StringUtils.substringBefore(ipAddress, ",");
    }

    private static DbSearcher searcher;
    private static Method method;

    /**
     * 在服务启动时加载 ip2region.db 到内存中
     */
    @PostConstruct
    private void initIp2regionResource() throws Exception {
        InputStream inputStream = new ClassPathResource("/ip/ip2region.db").getInputStream();
        //将 ip2region.db 转为 ByteArray
        byte[] dbBinStr = FileCopyUtils.copyToByteArray(inputStream);
        DbConfig dbConfig = new DbConfig();
        searcher = new DbSearcher(dbConfig, dbBinStr);
        //二进制方式初始化 DBSearcher,需要使用基于内存的查找算法 memorySearch
        method = searcher.getClass().getMethod("memorySearch", String.class);
    }

    /**
     * 获取ip地址的归属地
     */
    public static String getIpSource(String ipAddress) {
        if (ipAddress == null || !Util.isIpAddress(ipAddress)) {
            log.error("Error: Invalid ip address");
            return "";
        }
        try {
            DataBlock dataBlock = (DataBlock) method.invoke(searcher, ipAddress);
            String ipInfo = dataBlock.getRegion();
            if (!StringUtils.isEmpty(ipInfo)) {
                ipInfo = ipInfo.replace("|0", "");
                ipInfo = ipInfo.replace("0|", "");
                return ipInfo;
            }
        } catch (Exception e) {
            log.error("getCityInfo exception:", e);
        }
        return "";
    }

    public static String getIpProvince(String ipSource) {
        String[] strings = ipSource.split("\\|");
        if (strings[1].endsWith("省")) {
            return StringUtils.substringBefore(strings[1], "省");
        }
        return strings[1];
    }

    /**
     * 获取访问设备
     */
    public static UserAgent getUserAgent(HttpServletRequest request) {
        return UserAgent.parseUserAgentString(request.getHeader("User-Agent"));
    }
}

3.2 定义限流注解

为了使用方便,我这里选择了注解的方式,这样在使用的时候只需要在需要进行限流的请求Controller上添加一个注解即可。就像这样:

请在此添加图片描述

自定义的限流注解其实很简单,主要包含限流的Key,限流周期以及请求计数器。当然,这些数据都是完全可以自定义的,并没有什么约定俗成,具体工具自己的业务需要决定就好。

import java.lang.annotation.*;
/**
 * @author: 八尺妖剑
 * @date: 2022/10/19 12:34
 * @email: ilikexff@gmail.com
 * @blog: https://www.waer.ltd
 * @Description: 自定义注解:接口限流
 */
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface RateLimit {
    /**
     * 限流的key
     */
    String key() default "limit";

    /**
     * 周期:单位秒
     * @return
     */
    int cycles() default 5;

    /**
     * 请求次数
     * @return
     */
    int count() default 1;
}

3.3 自定义拦截器

这里使用到了拦截器,主要作用就是拦截处理所有的请求进行拦截,主要用到的preHandle方法。所有的限流逻辑都在这里实现。所以这部分挺重要的。

/**
 * @author: 八尺妖剑
 * @date: 2022/10/19 12:38
 * @email: ilikexff@gmail.com
 * @blog: https://www.waer.ltd
 * @Description: 拦截器:处理接口限流
 */
@Component
public class RateLimitInterceptor implements HandlerInterceptor {
    @Resource
    private RedisTemplate<String,Integer> redisTemplate;
    @Autowired
    private EmailUtils emailUtils;
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 如果请求的是方法,则需要做校验
        if (handler instanceof HandlerMethod) {
            HandlerMethod handlerMethod = (HandlerMethod) handler;
            // 获取目标方法上是否有指定注解
            RateLimit rateLimit = handlerMethod.getMethodAnnotation(RateLimit.class);
            if (rateLimit == null) {
                //说明目标方法上没有 RateLimit 注解
                return true;
            }
            // 说明目标方法上有 RateLimit 注解,所以需要校验这个请求是不是在刷接口
            // 获取请求IP地址
            String ip = IpUtils.getIpAddress(request);
            // 请求url路径
            String uri = request.getRequestURI();
            //存到redis中的key
            String key = "RateLimit:" + ip + ":" + uri;
            // 缓存中存在key,在限定访问周期内已经调用过当前接口
            if (redisTemplate.hasKey(key)) {
                // 访问次数自增1
                redisTemplate.opsForValue().increment(key, 1);
                Integer count = redisTemplate.opsForValue().get(key);
                // 超出访问次数限制
                if (count > rateLimit.count()) {
                    String from = IpUtils.getIpSource(ip);
                    EmailDTO emailDTO = SendEmailForRateLimit(ip,uri,from);
                    CompletableFuture.supplyAsync(()->{
                        return count==50 ? true : false;
                    }).thenApplyAsync(num->CompletableFuture.supplyAsync(() -> {
                        //System.out.println("num:" + num);
                        if(num) {
                            emailUtils.sendHtmlMail(emailDTO);
                        }
                        return "邮件发送完成";
                    }));
                    throw  new BizException(StatusCodeEnum.RATE_LIMIT_REQUEST);
                }
                // 未超出访问次数限制,不进行任何操作,返回true
            } else {
                // 第一次设置数据,过期时间为注解确定的访问周期
                redisTemplate.opsForValue().set(key, 1, rateLimit.cycles(), TimeUnit.SECONDS);
            }
            return true;
        }
        //如果请求的不是方法,直接放行
        return true;
    }

代码中已经写了详细的注释,所以就不再具体展开,需要注意的是,其中涉及到邮件发送的部分是我自己增加的一个安全提醒的部分逻辑,所以这部分可以忽略掉,不算在限流逻辑中也是没有任何毛病的。


4.实际使用之后的效果

到这一步,所有的工作都完成了,前面也提到过使用是非常简单的,我们只需要在要进行限流的请求方法上加上注解@RateLimit(cycles = 125,count = 3)即可,至于括号内的限流参数,那就根据自己的需求设置了,比如我这里写的就是125秒内同一个IP只能进行3次请求,否则就会触发限流,请求拒绝。

正常请求

请在此添加图片描述

请求限流

请在此添加图片描述

Redis中记录的数据

请在此添加图片描述

注意,限流触发的提示信息建议自己写一个,我承认,我自己这个提示确实不太友好,这主要是当时被对面搞那么一出,就很气人,所以在语言提示上就有些不够友好,如果需要自定义,只需要修改下面的常量数据就可。

请在此添加图片描述


5.关于计数器限流方案的一些总结

通过上面一波湿滑操作,我们已经以通过计数器这种方式具体应用到了实际的项目中,但这并不是故事的结束,每一种方法都有它独到的优势,自然也会有自己的不足,对于计数器实现的限流方案,其实还是有不少问题的。

考虑下面这种情况:

​ 假设对于某一些接口的需求是每分钟允许的请求上限是100次,如果某用户在最后那第59秒最后几毫秒瞬间直接给你来100个请求,当这一秒结束之后,计数器完成清零工作,此时该用户在下一秒的时候又给你整100个请求过来,啪一下就过来了,很快啊,那么1秒内这个很皮的用户就发送了2倍的请求,显然在这个情况下,一切也都是符合计数器限流原理的。
​ 这就是该方法的缺陷(不能很好的处理时间单位的边界),这种情况的存在,可能会导致系统一不小心就承受了太多,甚至击穿系统,所以这也是为什么还有其他几种方案的原因之一。

至此就完成了一次接口限流的操作实践。最后,纸上得来终觉浅啊哥

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/520509.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

MySql下载安装及使用

1.MySql下载 下载页 解压到想要安装的目录 2.配置系统环境 以管理员身份运行cmd命令行&#xff1a;输入mysql 回车&#xff0c;输出以下内容&#xff0c;表示mysql安装好了 3.初始化mysql(不设置无密码)&#xff0c;执行以下命令 mysqld --initialize-insecure执行这个命…

web学习笔记(五十三)身份认证

目录 1.Web 开发模式 1.1 服务端渲染的 Web 开发模式 1.2 服务端渲染的优缺点 1.3 前后端分离的 Web 开发模式 1.4 如何选择 Web 开发模式 2. 身份认证 2.1 Session 认证机制 3. 在 Express 中使用 Session 认证 3.1 安装express-session 中间件 3.2 配置 express-ses…

课程16 吸收·色散·散射(视频P55-P60)

吸收 色散 散射 吸收、色散、散射并称为分子光学&#xff1b;在一介质中&#xff0c;光的强度随传播距离而减少的现象&#xff0c;称为介质对光的吸收&#xff1b;介质的不均匀性将导致光的散射&#xff0c;散射到各个方向。光在介质中的传播速度小于真空光速&#xff0c;最终…

dcm文件数据学习

simpleITK读取数据 import SimpleITK as sitk import numpy as np import matplotlib.pyplot as plt base_path "/Users/yxk/Desktop/test/" image sitk.ReadImage(base_path"000000.dcm") # type(image) <class SimpleITK.SimpleITK.Image> imag…

789. 数的范围 (二分学习)左端大右,右端小左

题目链接https://www.acwing.com/file_system/file/content/whole/index/content/4317/ 当求左端点时&#xff0c;条件是a【mid】大于等于x&#xff0c;并把右端点缩小。 当求右端点时&#xff0c;条件是a【mid】小于等于x&#xff0c;并把左端点扩大。 1.确定一个区间&…

机器人力觉控制(力源)原理及力矩传感器性能分析

机器人力控原理及其性能分析 在机器人的操作任务中&#xff0c;处理机器人和环境之间的物理接触是非常重要的。由于机器人系统的复杂性和不确定性&#xff0c;纯运动控制往往是不够的&#xff0c;因为即使是最精确的模型也无法完全准确地预测所有可能的情况。 当机器人在与环境…

Qt5.15以上版本在线安装步骤,可选择更多早期版本

以ubuntu系统为例&#xff1a; 1、先去下载在线安装程序&#xff1a; https://download.qt.io/official_releases/online_installers/ 选择合适的版本&#xff0c;这里是在x64机器的ubuntu虚拟机里安装QT&#xff0c;所以选择如下版本&#xff1a; 或者直接在终端执行如下命令…

56.合并区间

这个C代码实现了一个名为Solution的类&#xff0c;其中有一个公共成员函数merge&#xff0c;该函数接收一个二维整数向量&#xff08;表示一系列闭区间&#xff09;&#xff0c;并返回一个新的二维整数向量&#xff0c;其中包含所有已合并的不重叠区间。 // 定义一个名为Solut…

【Java+Springboot】----- 通过Idea快速创建SpringBoot项目操作方法

一、第一步&#xff1a; 点击选择【File】->【New】-> 【Project】 最后弹出[new Project]界面。 二、第二步&#xff1a; 1. 选择【Spring Initializr】 2. 然后选择【Project SDK】的版本 3. 然后 Choose Initializr Service URL 选择默认&#xff08;Default&#x…

【漏洞复现】畅捷通T+ KeyInfoList.aspx SQL注入漏洞

免责声明&#xff1a;文章来源互联网收集整理&#xff0c;请勿利用文章内的相关技术从事非法测试&#xff0c;由于传播、利用此文所提供的信息或者工具而造成的任何直接或者间接的后果及损失&#xff0c;均由使用者本人负责&#xff0c;所产生的一切不良后果与文章作者无关。该…

pcb封装的丝印大小

下面的内容是我自己的看法&#xff0c;观点不同欢迎评论区讨论。 个人结论&#xff1a; 0402:4.1*37 0603:5*45 0805:6.6*60 1206:10.5*95 设计PCB时常用的封装有0402、0603、0805、1206不同的封装我认为应该对应不同的丝印大小。便于后期维修焊接&#xff0c;但是具体用…

[挖坟]如何安装Shizuku和LSPatch并安装模块(不需要Root,非Magisk)

2023年12月13日&#xff0c;LSPatch 停止维护 2024年1月8日&#xff0c;LSPosed 停止维护 2024年1月8日&#xff0c;ZygiskNext 停止维护 2024年1月9日&#xff0c;KernelSU 停止维护 这里使用 ColorOS 14 演示&#xff0c;其他品牌手机类似 安装 Shizuku 官网: https://shiz…

【论文通读】AgentStudio: A Toolkit for Building General Virtual Agents

AgentStudio: A Toolkit for Building General Virtual Agents 前言AbstractMotivationFramework评估GUI GroudingReal-World Cross-Application Benchmark Suite Conclusion 前言 来自昆仑万象的一篇智能体环境数据大一统框架工作&#xff0c;对未来计算机智能体的发展具有指…

京东云服务器地域和可用区选择方法,多因素考虑攻略

京东云服务器地域如何选择&#xff1f;根据地理位置就近选择地域。京东云主机地域支持北京、宿迁、上海和广州&#xff0c;华北地区用户选择北京地域&#xff0c;华东地区用户可以选择上海或宿迁地区&#xff0c;南方用户选择广州地域。云服务器吧yunfuwuqiba.com整理京东云主机…

PCI总线学习笔记:读写篇

前言 最近在写E1000网卡的驱动&#xff0c;这其中涉及到了PCI总线的相关内容。但是网上大部分关于PCI的文章都只局限在概念上的描述&#xff0c;并没有给出具体的例子来解释。这其实也是情理之中的&#xff0c;因为PCI总线规范就像是一个抽象的接口&#xff0c;其具体怎么实现…

SnapGene:解码生命之链的强大工具 mac/win版

在生物科技日新月异的时代&#xff0c;DNA分析已成为众多科研领域不可或缺的工具。SnapGene DNA生物分析软件应运而生&#xff0c;以其强大的功能和直观的用户界面&#xff0c;赢得了众多科研人员的青睐。 SnapGene软件获取 SnapGene是一款功能全面的DNA序列分析工具&#xff…

DevC++进行调试时,控制台出来后就闪退怎么解决

工具->编译选项->代码生成->连接器&#xff0c;然后将调试信息改成yes就可以了 如果有帮助麻烦点个赞呗

【好书推荐-第十四期】《 互联网大厂晋升指南:从P5到P9的升级攻略》

&#x1f60e; 作者介绍&#xff1a;我是程序员洲洲&#xff0c;一个热爱写作的非著名程序员。CSDN全栈优质领域创作者、华为云博客社区云享专家、阿里云博客社区专家博主、前后端开发、人工智能研究生。公众号&#xff1a;洲与AI。 &#x1f388; 本文专栏&#xff1a;本文收录…

0基础没编程经验能学嵌入式吗?

0基础没编程经验能学嵌入式吗&#xff1f; 可以的&#xff0c;即使你是0基础&#xff0c;没有编程经验&#xff0c;也完全有可能学习嵌入式系统。嵌入式系统是计算机技术与特定应用领域相结合的产物&#xff0c;涉及硬件和软件的知识。从零开始学习嵌入式开发&#xff0c;你可…

归并排序解读

在算法领域中&#xff0c;排序算法一直是一个核心话题。归并排序&#xff08;Merge Sort&#xff09;作为一种典型的分治思想应用&#xff0c;以其稳定、高效的特点受到了广泛的关注和应用。本文将深入探讨归并排序的原理、实现方式&#xff0c;以及它在实际应用中的价值。 一…