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

本文涉及的产品
MSE Nacos/ZooKeeper 企业版试用,1600元额度,限量50份
任务调度 XXL-JOB 版免费试用,400 元额度,开发版规格
注册配置 MSE Nacos/ZooKeeper,182元/月
简介: 在工作中,接口防盗刷至关重要,尤其对于短信发送等高风险接口。本文以发送短信接口为例,探讨了仅校验手机号的局限性,并提出基于Ticket机制的解决方案。客户端需先申请Ticket,服务端通过UserAgent、IP等判断请求合法性,生成加密的Ticket。客户端携带Ticket调用接口,并可能需通过图形验证码验证。此方案实现限流、幂等性和防盗刷,适用于多种接口,提升安全性。同时,建议减少明显错误提示,增加破解难度。

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

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

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

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

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

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

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

公众号: 后端随笔

个人博客:https://knowledge.xcye.xyz/

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

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

{
   
   
"phone": "11位手机号"
}

image.png

通过上面两个对比,我们不难发现,先对于只有一个参数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. 服务端对客户端请求进行验证
  1. UserAgent是否在黑名单中(恶意请求的UserAgent基本上都是同一个),UserAgent还可以有很多的玩法,比如类似于Ip一样,对UserAgent进行限流(会影响一部分正常用户)
  2. 从请求头中对用户身份进行初步识别。可以和客户端协商好,在一些请求头值上做文章,帮助服务端识别请求者身份
  3. 对IP进行识别。很多的恶意请求都来自于不同的Ip,有部分来自同一个网段,我们可以对Ip结合serviceType进行限制。

    1. 如果服务端识别请求是恶意请求,则在响应体中将captchaStatus设置为true,表示需要客户端进行图形验证码验证
    2. 下一步,服务端通过serviceType,从配置中获取限流规则。通过serviceType+primaryKey作为key,看是否能通过指定的限流。
    3. 通过限流后,服务端使用对称加密对{captchaStatus, primaryKey}进行加密,得到Ticket。这一步的目的是为了在最终验证Ticket时,从解密的数据中获取captchaStatus,避免captchaStatus是由客户端传递,从而解决请求绕过图形验证码验证问题,客户端根据captchaStatus判断该Ticket是否需要用户通过图形验证码,才能执行后续操作。
    4. 服务端将Ticket放入Redis,并且设置过期时间,然后将{ticket, captchaStatus}返回给客户端。

    image.png

服务端返回的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. 客户端接收到图形验证码数据并加载

    image.png

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

服务端验证Ticket

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

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

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

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

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

image.png

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

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

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

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

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

  2. 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都是加密的,在加密和解密的过程中,性能消耗也是一个可以优化的点,具体得看自己选择的算法是什么。

目录
相关文章
|
API
code: 400, value is mandatory for this action
code: 400, value is mandatory for this action
4301 1
|
存储 运维 开发工具
警惕日志采集失败的 6 大经典雷区:从本地管理反模式到 LoongCollector 标准实践
本文探讨了日志管理中的常见反模式及其潜在问题,强调科学的日志管理策略对系统可观测性的重要性。文中分析了6种反模式:copy truncate轮转导致的日志丢失或重复、NAS/OSS存储引发的采集不一致、多进程写入造成的日志混乱、创建文件空洞释放空间的风险、频繁覆盖写带来的数据完整性问题,以及使用vim编辑日志文件导致的重复采集。针对这些问题,文章提供了最佳实践建议,如使用create模式轮转日志、本地磁盘存储、单线程追加写入等方法,以降低日志采集风险,提升系统可靠性。最后总结指出,遵循这些实践可显著提高故障排查效率和系统性能。
769 20
|
Web App开发 索引
正则表达式匹配域名、网址、url
DNS规定,域名中的标号都由英文字母和数字组成,每一个标号不超过63个字符,也不区分大小写字母。标号中除连字符(-)外不能使用其他的标点符号。级别最低的域名写在最左边,而级别最高的域名写在最右边。由多个标号组成的完整域名总共不超过255个字符。
31033 0
|
Web App开发 域名解析 缓存
如何在 Ubuntu 20.04 上安装 Node.js 和 npm
本文我们主要为大家介绍在 Ubuntu 20.04 上安装 Node.js 和 npm 的三种不同的方式。
163169 7
如何在 Ubuntu 20.04 上安装 Node.js 和 npm
|
NoSQL Redis 数据库
阿里云Redis集群版简要介绍
产品简介 云数据库 Redis 提供集群版实例,轻松突破 Redis 自身单线程瓶颈,可极大满足对于 Redis 大容量或高性能的业务需求。 云数据库 Redis 集群版内置数据分片及读取算法,整体过程对用户透明,免去用户开发及运维 Redis 集群的烦恼。
13858 0
|
9月前
|
搜索推荐 NoSQL Java
微服务架构设计与实践:用Spring Cloud实现抖音的推荐系统
本文基于Spring Cloud实现了一个简化的抖音推荐系统,涵盖用户行为管理、视频资源管理、个性化推荐和实时数据处理四大核心功能。通过Eureka进行服务注册与发现,使用Feign实现服务间调用,并借助Redis缓存用户画像,Kafka传递用户行为数据。文章详细介绍了项目搭建、服务创建及配置过程,包括用户服务、视频服务、推荐服务和数据处理服务的开发步骤。最后,通过业务测试验证了系统的功能,并引入Resilience4j实现服务降级,确保系统在部分服务故障时仍能正常运行。此示例旨在帮助读者理解微服务架构的设计思路与实践方法。
477 17
|
10月前
|
Cloud Native 安全 Java
铭师堂的云原生升级实践
铭师堂完整经历了云计算应用的四个关键阶段:从”启动上云”到”全量上云”,再到”全栈用云”,最终达到”精益用云”。通过 MSE 云原生网关的落地,为我们的组织带来了诸多收益,SLA 提升至100%,财务成本降低67%,算力成本降低75%,每次请求 RT 减少5ms。
铭师堂的云原生升级实践
|
SQL 存储 关系型数据库
MySQL数据库——锁-表级锁(表锁、元数据锁、意向锁)
MySQL数据库——锁-表级锁(表锁、元数据锁、意向锁)
832 0
|
人工智能 前端开发 Java
【实操】Spring Cloud Alibaba AI,阿里AI这不得玩一下(含前后端源码)
本文介绍了如何使用 **Spring Cloud Alibaba AI** 构建基于 Spring Boot 和 uni-app 的聊天机器人应用。主要内容包括:Spring Cloud Alibaba AI 的概念与功能,使用前的准备工作(如 JDK 17+、Spring Boot 3.0+ 及通义 API-KEY),详细实操步骤(涵盖前后端开发工具、组件选择、功能分析及关键代码示例)。最终展示了如何成功实现具备基本聊天功能的 AI 应用,帮助读者快速搭建智能聊天系统并探索更多高级功能。
4172 2
【实操】Spring Cloud Alibaba AI,阿里AI这不得玩一下(含前后端源码)
|
存储 SQL 分布式计算
impala入门(一篇就够了)
impala入门(一篇就够了)
2468 0
impala入门(一篇就够了)

热门文章

最新文章