前言
原理
工程
自我提问
接口自由
时间逻辑漏洞
路径参数问题
真实ip获取
总结

前言


本文旨在详细介绍如何通过使用Interceptor和Redis来实现接口访问防刷的Demo。为避免过度简化,我们将逐步查找问题并逐步完善解决方案。

原理

该系统通过两种方式来防止接口刷访问:

第一种方式是通过 IP 地址和 URI 拼接,用于作为访问者访问接口区分。这种方式可以有效地防止同一用户多次访问同一接口。

第二种方式是通过拦截请求,在 Interceptor 中进行处理。系统会从 Redis 中统计用户访问接口的次数,并且在达到一定的阈值时,拒绝用户访问该接口。这种方式可以有效地防止接口被恶意访问。如下图所示:

java 如何防止相同参数的接口重复调用 java接口怎么防止被刷_开发语言


工程

该工程是一个接口保护工具,旨在提高接口的安全性。目前工程的地址位于:

https://github.com/Tonciy/interface-brush-protection

可以通过Apifox访问该工程,密钥为Lyh3j2Rv。

在该工程中,Interceptor处的代码处理逻辑显得尤为重要。Interceptor是一个拦截器,用于拦截请求并处理相关逻辑。虽然它只有几百行代码,但它却是整个工程的核心所在。

java 如何防止相同参数的接口重复调用 java接口怎么防止被刷_前端_02


java 如何防止相同参数的接口重复调用 java接口怎么防止被刷_前端_03

在多长时间内访问接口多少次,以及禁用的时长,则是通过与配置文件配合动态设置

java 如何防止相同参数的接口重复调用 java接口怎么防止被刷_java_04

 

当处于禁用时直接抛异常则是通过在ControllerAdvice处统一处理。这一做法可以确保代码的可读性和可维护性,因为它可以将异常处理逻辑集中在一个地方。此外,错误处理可以根据具体的业务需求进行定制,从而提高应用程序的可靠性和健壮性。

在实现这一方法时,需要注意代码的规范性和简洁性。可以使用try-catch块来捕获异常,然后在ControllerAdvice处进行处理。此外,可以考虑通过记录日志等方式来进行错误处理。这些措施可以帮助开发人员快速定位和解决问题,提高应用程序的质量和稳定性。

java 如何防止相同参数的接口重复调用 java接口怎么防止被刷_数据库_05


下面是一些测试(可以把项目通过Git还原到“【初始化】”状态进行测试)

防刷接口映射路径修改后维护问题

那么,这样的话,【7-12】也可以放置

而【7-12】这段时间有4次请求,就达到了我们禁用的条件了

是不是感觉怪怪的

想过其他做法,但是好像严格意义上真的做不到我所说的那样(至少目前来说想不到)

之前我们的做法,正常来说也够用,至少说有达到防刷的作用

后面有机会的话再看看,不知道我是不是钻牛角尖了

按照上述逻辑走,实际上也就是说当出现首次访问时,当做这5秒时间片段的起始

第2秒是,第8秒也是

但是有没有想过,实际上这个5秒时间片段实际上是可以放置在时间轴上任意区域的

上述情况我们是根据请求的到来情况人为的把它放在【2-7】,【8-13】上

而实际上这5秒时间片段是可以放在任意区域的

  • 正常访问时:

  • 访问次数过于频繁时:

java 如何防止相同参数的接口重复调用 java接口怎么防止被刷_数据库_06


自我提问

上述实现虽然已经达到了我们的接口防刷的目的,但是我们还有更多的事情可以做来提高系统的安全性。例如,我们可以增加更严格的鉴权机制,或者加入一些复杂的算法来防止恶意攻击。这些措施可以让我们的系统更加强大和稳定。

为了更好地实现这些措施,我们在项目中新增了一些补充的Controller。这些Controller将帮助我们更好地管理我们的系统,并且使我们的系统更加安全、可靠。如下所示:


简单来说就是,我们有两个Controller,分别是PassCotroller和RefuseController。这两个Controller分别有对应的get、post、put和delete类型的方法。这些方法可以帮助我们处理各种请求,以便保证我们的应用程序能够高效地运行。除此之外,这些方法的映射路径与方法名称一致,这使得它们非常易于使用。因此,通过使用这些Controller和它们的方法,我们可以更好地管理我们的应用程序。我们可以通过这些方法来处理各种请求,包括添加、修改和删除数据等。这将使我们的应用程序更加功能强大,也更加易于使用和维护。

  • 接口自由

    对于上述实现,我们可以看到,现在的接口防刷处理只针对所有接口。但是在实际开发中,可能并不是所有接口都需要防刷,这一点在项目案例中并没有体现出来。这也会引发问题:对于不需要防刷的接口,我们是否也需要做防刷处理呢?
    实际上,在实际工作中,我们需要对不同接口进行不同的处理,而不是简单地针对所有接口进行统一处理。因此,我们需要想出一种不同于上述实现的解决方案。现在,我想到了两种可能的解决方案:
  • 拦截器映射规则

    项目通过Git还原到"【Interceptor设置映射规则实现接口自由】"版本即可得到此案例实现
    我们都知道拦截器是可以设置拦截规则的,从而达到拦截处理目的

  • 1.这个AccessInterfaceInterceptor是专门用来进行防刷处理的,那么实际上我们可以通过设置它的映射规则去匹配需要进行【接口防刷】的接口即可
    2.比如说下面的映射配置
    3.这样就初步达到了我们的目的,通过映射规则的配置,只针对那些需要进行【接口防刷】的接口才会进行处理
    4.至于为啥说是初步呢?下面我就说说目前我想到的使用这种方式进行【接口防刷】的不足点:
    所有要进行防刷处理的接口统一都是配置成了 x 秒内 y 次访问次数,禁用时长为 z 秒
  • 要知道就是要进行防刷处理的接口,其 x, y, z的值也是并不一定会统一的
  • 某些防刷接口处理比较消耗性能的,我就把x, y, z设置的紧一点
  • 虽然说防刷接口的映射路径基本上定下来后就不会改变
  • 但实际上前后端联调开发项目时,不会有那么严谨的Api文档给我们用(这个在实习中倒是碰到过,公司不是很大,开发起来也就不那么严谨,啥都要自己搞,功能能实现就好)
  • 也就是说还是会有那种要修改接口的映射路径需求
  • 当防刷接口数量特别多,后面的接手人员就很痛苦了
  • 就算是项目是自己从0到1实现的,其实有时候项目开发到后面,自己也会忘记自己前面是如何设计的
  • 而使用当前这种方式的话,谁维护谁蛋疼
  • 而某些防刷接口处理相对来说比较快,我就把x, y, z 设置的松一点
  • 这没问题吧
  • 但是现在呢?x, y, z值全都一致了,这就不行了
  • 这就是其中一个不足点
  • 当然,其实针对当前这种情况也有解决方案
  • 那就是弄多个拦截器
  • 每个拦截器的【接口防刷】处理逻辑跟上述一致,并去映射对应要处理的防刷接口
  • 唯一不同的就是在每个拦截器内部,去修改对应防刷接口需要的x, y, z值
  • 这样就是感觉会比较麻烦
    自定义注解 + 反射
    怎么说呢
  • 就是通过自定义注解中定义 x 秒内 y 次访问次数,禁用时长为 z 秒
  • 自定义注解 + 在需要进行防刷处理的各个接口方法上
  • 在拦截器中通过反射获取到各个接口中的x, y, z值即可达到我们想要的接口自由目的

  • 下面做个实现
    声明自定义注解
  • java 如何防止相同参数的接口重复调用 java接口怎么防止被刷_java_07


  • Controlller中方法中使用

  • Interceptor处逻辑修改(最重要是通过反射判断此接口是否需要进行防刷处理,以及获取到x, y, z的值)
  • java 如何防止相同参数的接口重复调用 java接口怎么防止被刷_数据库_08

  •  
  • 由于不方便演示,这里暂时不贴测试结果图片。但是,您可以将项目还原到"【自定义主键+反射实现接口自由"版本,以获得此案例的实现。您也可以对接口进行测试,以检查是否实现了自定义x、y和z的效果。
    现在,我们可以针对每个需要进行防刷处理的接口进行针对性的自定义。我们可以设置每个接口的最大访问次数,以及禁用时长。只需要在相应的接口方法中添加代码即可。
    总体而言,这种实现方法还不错,网上也有很多相关资料。但是,我们仍然需要改进。
    让我们以PassController为例。下面是它的实现代码。

  • 下图是其映射路径关系
  • java 如何防止相同参数的接口重复调用 java接口怎么防止被刷_java_09

  •  
  • 同一个Controller的所有接口方法映射路径的前缀都包含了/pass
    这样的设计有一个好处,就是我们可以通过注解@ReqeustMapping在类上标记映射路径/pass,这样所有的接口方法前缀都包含了/pass。这样一来,我们就可以很方便地修改映射路径前缀了,只需要修改这一部分即可。
    这也是我们在使用SpringMVC时最常见的用法。
    但是,我们能否将这种设计思想应用到自定义注解中呢?让我们来看一个具体的需求。
    假设PassController中的所有接口都需要进行防刷处理,且它们的x、y、z值都是一样的。如果我们的自定义注解仍然只能加载在方法上,那么一个个接口都需要加上这个注解,这无疑非常繁琐。
    不过,我们可以很轻松地解决这个问题。首先,我们需要修改自定义注解,让它可以作用在类上。

接着就是修改AccessLimitInterceptor的处理逻辑

AccessLimitInterceptor中代码修改的有点多,主要逻辑如下


与之前实现比较,不同点在于x, y, z的值要首先尝试在目标类中获取

其次,一旦类中标有此注解,即代表此类下所有接口方法都要进行防刷处理

如果其接口方法同样也标有此注解,根据就近优先原则,以接口方法中的注解标明的值为准

java 如何防止相同参数的接口重复调用 java接口怎么防止被刷_java_10


  • 好了,这样就达到我们想要的效果了

    项目通过Git还原到"【自定义注解+反射实现接口自由-版本2.0】"版本即可得到此案例实现,自己可以测试万一下
    这是目前来说比较理想的做法,至于其他做法,暂时没啥了解到

时间逻辑漏洞

这是我一开始都有留意到的问题

也是一直搞不懂,就是我们现在的所有做法其实感觉都不是严格意义上的x秒内y次访问次数

特别注意这个x秒,它是连续,任意的(代表这个x秒时间片段其实是可以发生在任意一个时间轴上)

我下面尝试表达我的意思,但是我不知道能不能表达清楚

假设我们固定某个接口5秒内只能访问3次,以下面例子为例

 

java 如何防止相同参数的接口重复调用 java接口怎么防止被刷_开发语言_11


  • 底下的小圆圈代表此刻请求访问接口
    按照我们之前所有做法的逻辑走
  • 第2秒请求到,为首次访问,Redis中统计次数为1(过期时间为5秒)
  • 第7秒,此时有两个动作,一是请求到,二是刚刚第二秒Redis存的值现在过期
  • 我们先假设这一刻,请求处理完后,Redis存的值才过期
  • 按照这样的逻辑走
  • 第七秒请求到,Redis存在对应key,且不大于3, 次数+1
  • 接着这个key立马过期
  • 再继续往后走,第8秒又当做新的一个起始,就不往下说了,反正就是不会出现禁用的情况

路径参数问题

假设现在PassController中有如下接口方法

真实ip获取

在之前的代码中,我们获取代码都是通过request.getRemoteAddr()获取的

但是后续有了解到,如果说通过代理软件方式访问的话,这样是获取不到来访者的真实ip的

至于如何获取,后续我再研究下http再说,这里先提个醒

  • 也就是我们在接口方法中常用的在请求路径中获取参数的套路
    但是使用路径参数的话,就会发生问题
    那就是同一个ip地址访问此接口时,我携带的参数值不同
    按照我们之前那种前缀+ip+uri拼接的形式作为key的话,其实是区分不了的
    下图是访问此接口,携带不同参数值时获取的uri状况

  • 这样的话在我们之前拦截器的处理逻辑中,会认为是此ip用户访问的是不同的接口方法,而实际上访问的是同一个接口方法
    也就导致了【接口防刷】失效
    接下来就是解决它,目前来说有两种
  • 不要使用路径参数
  • 这算是比较理想的做法,相当于没这个问题
    但有一定局限性,有时候接手别的项目,或者自己根本没这个权限说不能使用路径参数
  • 替换uri
  • 我们获取uri的目的,其实就是为了区别访问接口
  • 而把uri替换成另一种可以区分访问接口方法的标识即可
  • 最容易想到的就是通过反射获取到接口方法名称,使用接口方法名称替换成uri即可
  • 当然,其实不同的Controller中,其接口方法名称也有可能是相同的
  • 实际上可以再获取接口方法所在类类名,使用类名 + 方法名称替换uri即可
  • 实际解决方案有很多,看个人需求吧
  • 总结
    在我的开发过程中,我一开始只考虑了【接口防刷】的功能,只是想统计访问次数。但是在我阅读了其他人的实现方式之后,我开始思考,这个功能还可以怎么扩展呢?
    于是我开始研究自定义注解+反射这种实现方式。以前我对注解+反射的实现方式并不太了解。但是在我自己动手实践过程中,我渐渐明白了它的作用。我发现注解+反射的实现方式不仅可以用于数据报表导出和基本权限控制,还可以用于【接口防刷】。
    写博客真的是件挺有意义的事情,它可以让你更深入地了解某个知识点。在我研究【接口防刷】的过程中,我发现知识是相关联的。当你探索某个知识点时,你会牵扯到其他知识点,比如之前我写的【单例模式】实现。一开始我只了解到懒汉式和饿汉式,但是当我深入研究时,我发现还有序列化/反序列化、反射调用生成实例、对象克隆等方式可以破坏单例模式,而这些问题又是如何解决的呢?
    这个过程让我不断进步,为了保证线程安全问题,我还需要考虑synchronized、volatile关键字等问题。而这些问题又会牵扯到JVM、JUC、操作系统等方面的知识。总之,写博客可以让你不断深入了解某个知识点,并且让你的知识更加完整和相关联。