Loading [MathJax]/jax/output/CommonHTML/config.js
前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >一种优雅的方式整合限流、幂等、防盗刷

一种优雅的方式整合限流、幂等、防盗刷

原创
作者头像
xcye
修改于 2024-09-01 08:16:06
修改于 2024-09-01 08:16:06
28903
代码可运行
举报
运行总次数:3
代码可运行

大家在工作中肯定遇到过接口被人狂刷的经历,就算没有经历过,在接口开发的过程中,我们也需要对那些容易被刷的接口或者和会消耗公司金钱相关的接口增加防盗刷功能。例如,发送短信接口以及发送邮件等接口,我看了国内很多产品的短信登录接口,基本上都是做了防盗刷,如果不做的话,一夜之间,也许公司都赔完了┭┮﹏┭┮。

假设我们正在开发一个发送短信(仅国内)的接口,过程如下

  1. 接口定义为/sendSms
  2. 请求参数只有phone
  3. 在处理请求时,我们对请求参数phone进行了合法性校验
  4. 如果手机号合法,那么调用腾讯云等服务商的发送短信Api,向目标手机号发送短信
  5. 流程结束

上面便是一个最简单的向手机号发送短信验证码的接口,不考虑其他和业务相关的操作。我们现在来分析一下,该接口存在的问题(刷接口)。

  1. 只对请求参数中的手机号进行合法性校验(11位手机号),并没有对手机号是否为空号进行验证,会导致别人构造大量合法但是是空号的手机号
  2. 没有增加单个手机号,每天最大发送次数
  3. 没有控制每个手机号发送间隔,会导致同一时间,向相同手机号发送大量短信

既然我们知道了发送短信验证码接口存在的缺陷,那我们将这些问题一一解决了,是不是就可以避免接口被盗刷呢?答案是只能在一定程度上防止被盗刷,因为这些恶意请求中,手机号都是通过程序无限生成的,都能通过我们的正则校验,所以对手机号进行发送次数和发送间隔限制,对他们是没有任何效果的。另外,想要避免向空号手机号发送短信的话,还需要额外的引入第三方的空号检验Api,增加了新的资源消耗。

我们现在从发送短信验证码的接口转移到其他的接口来看看,寻找一种能够应用于所有的接口,并能实现限流,幂等,防盗刷功能的方案。

解决接口请求参数容易被构造

我们其实不难发现,导致接口被盗刷的根本原因在于请求参数很容易通过算法构造构造出来,这些通过程序生成的参数,在我们的程序看来,都是合法的。

代码语言:json
AI代码解释
复制
{

"phone": "11位手机号"

}
淘宝发送短信验证码
淘宝发送短信验证码

通过上面两个对比,我们不难发现,先对于只有一个参数phone的发送短信接口来说,想要构造出淘宝发送短信的参数,难度直接上升了很多个阶梯。

我们从解决接口请求参数容易被构造的角度出发,我目前能想到的只有对请求参数进行加密,使用非对称加密的方式。具体的思路为,客户端在发送请求之前,使用服务端提供的公钥对请求参数进行加密,让请求参数看上去不那么容易被构造出来。服务端获取到请求参数后,使用私钥进行解密,然后再进行后续的一些验证操作。

那么这样可以防止接口被盗刷么?答案是,只能防君子,不能防小人。特别是对于Web端来说,如果发起盗刷的这个人,同样是一个开发者,他直接F12就可以从js文件中找到公钥。对于App来说,获取源码的方式会更难一点,但是最终公钥应该还是能够被找到的。

如果我们解决公钥容易被获取的问题,是不是可以通过这种方式防止接口被盗刷呢?如果能够解决公钥容易被获取的问题,在一定程度上,确实是可以解决接口被盗刷的问题,但是现在又将问题转移到了获取公钥接口上,我们还是需要解决获取公钥接口被盗刷的问题。

而且如果获取到的公钥不能存在时效性,可以被多次使用,那么这些通过加密实现防盗刷的接口,在公钥被泄露的情况下,还是会存在被盗刷的问题。想要解决的话,可以让公钥只能使用一次,或者只能在很短时间内使用,再者只能被多少个请求使用。我最终的解决方案也是类似于这个,让令牌只能使用一次。

而且使用公钥进行加密,通常是防止在请求过程中发生的中间人攻击,是为了解决参数被修改以及泄露的问题。

Ticket机制

我最终并不是通过解决参数容易被构造来防止盗刷的,我是通过对请求进行是否是机器人判断,如果是非法请求,强制必须先通过图形验证码,只有合法的请求,服务端才会进行处理。

我基于Ticket机制,客户端在发送请求之前,必须先向服务端申请一个Ticket。服务端在处理申请Ticket请求时,对请求进行判断,判断包含了是否是恶意请求和是否需要进行限流。当这两步都通过后,服务端会生成一个被加密,存在时效性并且只能使用一次的Ticket,客户端发送真正请求时,需要携带这个Ticket。每个Ticket只能被使用一次,而且客户端每次都携带Ticket,还可以通过Ticket实现请求的幂等性。

这种方案并不和任何的接口耦合,Ticket是携带在请求头上,不会对请求参数造成污染。

申请Ticket

我最终是使用Ticket完成了限流,防盗刷,幂等性这三个功能,为了让这个功能更加的通用,不和任何的接口相耦合。在申请Ticket时,客户端需要传递两个参数,分别是serviceType和primaryKey。serviceType用于控制该接口的类型,而primaryKey会被用于限流。最终结合配置中心,做到了能够轻松的对任何类型的请求进行独立的限流,UserAgent黑名单与白名单,Ip限流等操作。

具体的执行过程为(以发送短信验证码为例):

  1. 客户端调用接口申请Ticket,传递的参数为{serviceType: sms, primaryKey: 用户手机号}
  2. 服务端对客户端请求进行验证
  3. UserAgent是否在黑名单中(恶意请求的UserAgent基本上都是同一个),UserAgent还可以有很多的玩法,比如类似于Ip一样,对UserAgent进行限流(会影响一部分正常用户)
  4. 从请求头中对用户身份进行初步识别。可以和客户端协商好,在一些请求头值上做文章,帮助服务端识别请求者身份
  5. 对IP进行识别。很多的恶意请求都来自于不同的Ip,有部分来自同一个网段,我们可以对Ip结合serviceType进行限制。
  6. 如果服务端识别请求是恶意请求,则在响应体中将captchaStatus设置为true,表示需要客户端进行图形验证码验证
  7. 下一步,服务端通过serviceType,从配置中获取限流规则。通过serviceType+primaryKey作为key,看是否能通过指定的限流。
  8. 通过限流后,服务端使用对称加密对{captchaStatus, primaryKey}进行加密,得到Ticket。这一步的目的是为了在最终验证Ticket时,从解密的数据中获取captchaStatus,避免captchaStatus是由客户端传递,从而解决请求绕过图形验证码验证问题,客户端根据captchaStatus判断该Ticket是否需要用户通过图形验证码,才能执行后续操作。
  9. 服务端将Ticket放入Redis,并且设置过期时间,然后将{ticket, captchaStatus}返回给客户端。
申请Ticket活动图
申请Ticket活动图

服务端返回的Ticket是加密后的密文,存在过期时间,保存在Redis中,并且只能被使用一次,无法被客户端构造出来。尽管加密算法被不小心泄露,服务端也无法从Redis中查询到这个"合法的Ticket",所以这个Ticket是足够安全的。

图形验证码

调用申请Ticket接口后,响应参数中包含两个参数:captchaStatus, ticket。captchaStatus表示该Ticket是否需要客户端通过图形验证码。

当captchaStatus为true时,客户端调用另一个接口加载图形验证码,在调用接口时,需要携带上一步获得的Ticket,服务端最终会将本次的图形验证码和Ticket进行绑定,最终实现在下一步中通过Ticket获取图形验证码的验证结果,具体步骤为:

  1. 客户端携带申请到的Ticket加载图形验证码数据
  2. 服务端从请求头中获取Ticket,从Db中查询该Ticket加载过几次图形验证码,如果超过最大加载次数,那么直接通知客户端重新申请新Ticket,并且删除和旧Ticket相关的数据。
  3. 验证通过后,生成图形验证码数据,得到该图形验证码的key,然后将key和ticket放入Db中存储起来,目的是为了保存图形验证码验证结果
  4. 客户端接收到图形验证码数据并加载
加载图形验证码活动图
加载图形验证码活动图

在防盗刷功能中,最有效的还得是验证码功能

服务端验证Ticket

当客户端完成上面两个后,客户端现在才开始调用真正的接口(发送短信)。在调用发送短信验证码时,客户端需要携带申请到的Ticket和图形验证码Key(如果captchaStatus为true)。

服务端接收到请求后,具体的处理步骤如下:

  1. 从请求中获取Ticket,并且对Ticket进行解密,从Redis中查询该Ticket是否存在

尽管我们的防盗刷逻辑被人知晓,他们也不能随意的构造Ticket

  1. 从解密后的数据中获取captchaStatus字段的值,如果为true,则表示该Ticket需要执行图形验证码验证。服务端从DB中查询和该Ticket最后一次绑定的图形验证码Key的结果,如果没有进行验证或者结果为失败的话,直接结束流程
  2. 对Ticket进行幂等性验证,主要是通过判断该Ticket之前是否被使用过,如果上一个请求已经完成,那么直接从Redis中获取执行结果,并返回
  3. 当上面都没有问题后,现在才开始执行最终的业务逻辑,这里是执行发送短信验证码。因为这个功能并不和任何的接口耦合,如果我们需要更细的防盗刷,还可以在具体的接口里面做文章。
  4. 执行完毕后,需要把Ticket相关的数据都删除。
服务端验证Ticket活动图
服务端验证Ticket活动图

上面便是我实现接口防盗刷的具体过程,现在我们来验证一下,这个防盗刷是否真的能防(还是以发送短信验证码)?

  1. 构造大量合法但空号的手机号

每次请求时,都需要先申请Ticket,primaryKey为手机号。因为这些恶意请求的UserAgent是相同的,如果我们预先接收到报警并且将UserAgent放入黑名单中,这些请求会直接被拦截。

就算UserAgent每被拦截,还有Ip等其他的限流措施。如果都通过,我们还可以直接强制要求每一个请求都进行图形验证码验证,因为图形验证码的破解难度更高,基本上已经劝退很多人了,强制进行图形验证码验证,对于正常用户来说,也只会降低使用体验。

对于手机号为空号来说,如果这个用户确实通过了上面这些措施,那么基本上可以保证他是一个真实用户,所以手机号是否为空号验证,我觉得是多此一举,除非发送短信的资源真的非常宝贵。

  1. Ticket被泄露,被伪造

在公司没出内鬼的情况下,Ticket是不可用被伪造出来的,并且就算被伪造出来,这个Ticket也没有保存至Db。如果该Ticket的captchaStatus为false并且被泄露了,他们也只能在指定时间内使用该Ticket,并且只能使用一次。不可能会存在Ticket无限泄露的情况。

在上面的过程中,服务端验证请求是否是机器人,还可以在发送真正请求时进行验证,如果验证失败,客户端根据响应体执行对应的操作,然后携带Ticket重发请求。上面的逻辑并没有对正常用户的验证结果进行缓存,这会导致,正常用户在调用这些接口时,每调用一次,都需要通过图形验证码。

其他措施

还有其他的措施,也可以增加接口被盗刷的情况。这些措施包括增加防盗刷逻辑被破解难度和防止接口被盗刷。

先说防止接口被盗刷,本质上是防止接口被泄露。对于App来说,某个人想要知道我们接口信息的话,必须对App进行反编译,我对App反编译不太了解,可以试试那些增大反编译的措施,就算不进行反编译,使用Fiddler工具也是可以看到请求信息的。对于Web端来说,用户只需要按F12就可以看到JavaScript代码,以及每个请求的参数,响应体等。我们可以禁用F12以及右键(降低用户体验),以及在生产环境中,添加当用户按F12后,自动进入无限Debug模式。这两个操作可以增加我们接口被暴露的风险,从而在一定程度上起到"防盗刷"目的。

对于增加防盗刷逻辑被破解难度来说,市场上有很多的App的限流等规则都被人攻破了,我个人觉得会被攻破,除了接口设计的原因外,还有一个是接口的响应体中提示了很明显的错误信息。比如我们访问某个增加了防刷功能的接口,该接口提示UserAgent无效,当前Ip已被限流,Ticket无效,未进行图形验证码验证等很明显的信息。这些信息其实已经间接提示了让请求变合法的步骤是什么,这虽然可以帮助开发人员进行调试,但也间接的帮助了那些发送恶意请求的人。所以为了增大防盗刷逻辑被破解的难度,我们不需要返回这些很明显的提示信息,可以无论什么原因,都返回"非法请求",对于公司开发人员来说,他们自己通过code从开发文档中查询每个code所代表的意思。

以上便是我对于防止接口被盗刷的一些见解,可能还有更优的方案,但是我目前确实只能想到这一种。另外,也可以使用已有的服务,比如腾讯云和阿里云等服务商的验证码。我使用的图形验证码是开源的,来自于dromara大佬开源的Java行文验证,码,使用起来非常的方便,并且支持滑块,旋转,滑动,文字点选,非常感谢大佬。此外,因为每次请求时申请到的Ticket都是加密的,在加密和解密的过程中,性能消耗也是一个可以优化的点,具体得看自己选择的算法是什么。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
学习Spring——依赖注入
前言:   又开始动笔开了“学习Spring”系列的头……   其实一开始写“学习SpringMVC”的几篇文章是出于想系统的了解下Spring以及SpringMVC,因为平时在公司中虽然每天都在使用Spring相关的框架或者其他,但是绝大部分都是已经写好配置文件,做好相关配置,而我们能做的就是写一些与业务逻辑有关的Controller层面或者Service层面的代码。毕竟所做的产品成熟了,或者说框架越来越成熟了,我们对于底层原理的东西关注的就少了,认识也浅了。   个人感觉,颇具讽刺意味的是,“Sprin
JackieZheng
2018/01/16
7800
学习Spring——依赖注入
Spring快速入门
最近几天一直在忙着学习数据结构和算法,关于JavaEE的框架也有一阵子没更新了。那么今天就来聊一聊Spring框架吧,针对该框架写一篇快速入门的文章。
wangweijun
2020/02/14
4650
Spring都没整明白,你还搞什么java开发?
本篇文章将带你掌握Spring框架,满满的干货,内容有点多,希望你能有耐心看完,看完后一定会有所收获。
淘课之家
2020/03/12
8120
Spring都没整明白,你还搞什么java开发?
spring-bean配置讲解
2.Bean的配置方式:通过全类名(反射)、通过工厂方法啊(静态方法&实例化工厂方法)、FactoryBean
Java开发者之家
2021/06/16
5260
Spring 框架学习(二)Spring Bean 的装配与注入
最初的引入案例便是在xml中的显式配置。从引入的步骤我们可以了解到,在使用 xml 显式的装配一个 Bean 时,需要以下几个步骤。
求和小熊猫
2020/11/25
6620
spring 配置bean[通俗易懂]
在spring IOC容器读取Bean配置创建Bean实例之前。必须对它进行实例化。仅仅有在容器实例化后,才干够从IOC容器里获取Bean实例并使用
全栈程序员站长
2022/07/07
4520
spring 配置bean[通俗易懂]
Spring框架:第二章:IOC依赖注入及40个实验
4、IOC依赖注入 4.1、什么是IOC(面试经常问) IOC 全称指的是 Inverse Of Control 控制反转。 控制反转是指将bean对象的创建权力交给spring来操作。在使用Spring以前 。都是通过手动代码new 对象();
Java廖志伟
2022/09/28
5580
Spring框架:第二章:IOC依赖注入及40个实验
Spring-依赖注入
此外Spring还支持工厂方法注入。 这篇博文我们将了解到不同注入方式的具体配置方法。
小小工匠
2021/08/16
5460
跟着柴毛毛学Spring(3)——简化Bean的配置
通过前面的学习,我们会感觉到对于一个有较多Bean的大项目,Spring的配置会比较复杂。那么接下来我们就介绍如何简化Spring的配置。 简化Spring的配置主要分为两类: 1. 自动装配 2. 自动扫描 下面就详细介绍这两种简化配置的方式。 自动装配 自动装配的种类 byName:根据属性的名字自动装配 byType:根据属性的类型自动装配 constructor:根据构造器的参数类型自动装配 autodetect:最佳自动装配。首先采用constructor自动装配,若没有发现与构造器相匹配
大闲人柴毛毛
2018/03/09
7740
Spring之对象依赖关系(依赖注入Dependency Injection)
别先生
2018/01/02
1.1K0
Spring系列四:Spring Bean(深度讲解)
Spring支持构造方法注入、属性注入、工厂方法注入,其中工厂方法注入,又可以分为静态工厂方法注入和非静态工厂方法注入。
叶秋学长
2022/07/25
5000
Spring系列四:Spring Bean(深度讲解)
Spring5参考指南:依赖注入
依赖注入就是在Spring创建Bean的时候,去实例化该Bean构造函数所需的参数,或者通过Setter方法去设置该Bean的属性。
程序那些事
2020/07/07
7280
Spring 学习笔记(四)—— XML配置依赖注入
  依赖注入(DI)与控制反转(IoC)是同一个概念,都是为了处理对象间的依赖关系。
Rekent
2018/09/04
3990
Spring中bean的注入方式
 首先,要学习Spring中的Bean的注入方式,就要先了解什么是依赖注入。依赖注入是指:让调用类对某一接口的实现类的实现类的依赖关系由第三方注入,以此来消除调用类对某一接口实现类的依赖。
Kevin_Zhang
2019/02/20
1.9K0
面试官常问的Spring依赖注入和Bean的装配问题,今天给大家讲清楚!
小伙伴儿们,Spring 的依赖注入以及 Bean 的装配是面试常问的知识点,今天我们来学习一下Spring中的依赖注入方式,以及如何将自己开发的Bean装配到Spring IoC容器中。
程序员的时光001
2020/10/10
1.5K0
面试官常问的Spring依赖注入和Bean的装配问题,今天给大家讲清楚!
spring笔记(一)
且可以和其他框架一起使用;Spring与Struts, Spring与hibernate
HUC思梦
2020/09/03
3970
Spring(二)-生命周期 + 自动装配(xml) +自动装配(注解)
参考博客: ApplicationContext和ConfigurableApplicationContext解析
化羽羽
2022/10/28
3830
Spring【依赖注入】就是这么简单
前言 在Spring的第二篇中主要讲解了Spring Core模块的使用IOC容器创建对象的问题,Spring Core模块主要是解决对象的创建和对象之间的依赖关系,因此本博文主要讲解如何使用IOC容器来解决对象之间的依赖关系! 回顾以前对象依赖 我们来看一下我们以前关于对象依赖,是怎么的历程 直接new对象 在最开始,我们是直接new对象给serice的userDao属性赋值… class UserService{ UserDao userDao = new UserDao(); } 写DaoF
Java3y
2018/03/15
8400
Spring【依赖注入】就是这么简单
Spring Bean 的装配方式以及Autowired与Resource的使用及区别
在Spring的使用中,如果要将一个bean实例化,可以通过配置文件,也可以通过在java代码里面的注解来实现,Spring能够根据自动协作这些bean之间的关系,这种自动协作的过程,也称之为自动装配。 自动装配模式有如下四种模式:
冬天里的懒猫
2021/09/08
7440
伙计,是时候拉近你和【Spring】之间的距离了!
大家好,我是小菜,一个渴望在互联网行业做到蔡不菜的小菜。可柔可刚,点赞则柔,白嫖则刚!死鬼~看完记得给我来个三连哦!
蔡不菜丶
2020/11/11
4750
伙计,是时候拉近你和【Spring】之间的距离了!
相关推荐
学习Spring——依赖注入
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
本文部分代码块支持一键运行,欢迎体验
本文部分代码块支持一键运行,欢迎体验