神代綺凛

借助 redis 实现访问控制
轻松做到基于 IP 或用户 ID 等的单位时间内访问频率的限制
扫描右侧二维码阅读全文
16
2020/03

借助 redis 实现访问控制

轻松做到基于 IP 或用户 ID 等的单位时间内访问频率的限制

Head Pic: #明日方舟 双狼 by 卧龙先生的爸爸 - pixiv

做开放 API,访问控制防止滥用是相当重要的一部分

我相信世上好人还是多,但不可避免地总会出现那么几个憨憨,普通 WAF 提供的基于 URL 的访问频率限制已经不顶用了,自己拓展又很麻烦

想了想,不如直接用开发 API 的语言写好了,方便、契合度还高

关于 redis

为什么用 redis

其实也没有什么特别的理由,只是碰巧想了解学习一下 redis,结果无意间发现 redis 很适合用来做访问记录,主要因为以下两点

  1. 支持有序集合 (sorted sets)
  2. 可为某个键设置 TTL (time to live)

这两点,无疑为做单位时间内的访问频率限制提供了巨大便利

另外明确一下,此处单位时间指的是持续时间而不是自然时间:
例如每天允许访问 1000 次,即从 1 天前的时间点到当前时间点的这段时间内最多允许访问 1000 次,而不是每个自然天中最多允许访问 1000 次,过了 0 点就刷新的那种

(如果是自然时间那好办了,直接开干就可以,根本用不着 redis)

redis 中的有序集合

Redis 中的有序集合是 string 类型元素的集合,与一般集合不同的是每个元素都与一个 double 值相关联用于排序,称作 score

有序集合中的元素不可重复,但 score 可以重复

思路及实现

以下例子均为 PHP 下的实现,用到 phpredis 扩展

基本思路

使用 redis 储存访问记录,当访问者访问时通过判断单位时间内该访问者的访问次数是否超出预订值来决定是否 block

确定如何储存

根据对 redis 中有序集合的基础了解,使用它储存访问记录之前我们需要确定三个参数

  • key - 键名
  • score - 用于排序
  • value - 唯一值

想一下,我们是要基于 IP用户 ID,记录单位时间内的访问行为,因此可以这么设计

  • key - IP 或用户 ID
  • score - 时间戳
  • value - 一个唯一值,可以理解为请求 ID,也可以包含任何你想记录的信息

由于 score 可以重复,因此我们不需要在意时间戳的单位,秒还是毫秒还是微秒都无所谓,只需要保证 value 不重复即可

正好,PHP 中有一个函数 uniqid,作用是生成一个基于当前时间微秒数的几乎唯一的 ID

* 通过随机前缀和more_entropy参数可以增加唯一性的概率,足够了,已经可以不用再努力了

确定如何释放

释放过时的记录是必要的,不然一直存下去内存迟早药丸,虽然也可能等不到这一天就是了(

此处就轮到 TTL 派上用场了,前面提到过 redis 支持为键设置 TTL,这样就可以在特定时间后自动释放,我们每次要向有序集合内添加新元素时都更新一次 TTL,将其复原至预设值,这样就可以保证活动状态下的数据不会被释放

但如果一个有序集合一直有数据添加进来怎么办,这样也会越存越多

好办,只要每次向这个集合添加元素前先删掉所有过期元素就好了

这样所有过期记录都只有两种下场:在新元素添加进来之前被删除,或因为其所属集合的 TTL 到期而被删除

简单实现

连接 redis 的步骤均省略

公共变量

$time = time();         // 当前时间戳(秒)
$ttl = 24 * 60 * 60;    // 一天内
$key = 'IP or User ID'; // 这个访问者
$limit = 1000;          // 最多允许访问 1000 次

$line = $time - $ttl;   // TTL 时间线

获取剩余额度信息

// 时间线后的记录数
$count = $redis->zCount($key, $line, '+inf');
// 拿到最接近时间线的 score 对应的 value,返回值是数组,没有就是空数组
$top_value = $redis->zRangeByScore($key, $line, '+inf', ['limit' => [0, 1]]);

// 最终要得到的:剩余额度
$quota = $limit - $count;
// 最终要得到的:额度恢复 1 所需等待的时间,如果没用过就 0
$quota_ttl = count($top_value) > 0 ? $redis->zScore($key, $top_value[0]) + $ttl - $time : 0;

记录使用行为

// 清除旧记录
$redis->zRemRangeByScore($key, '-inf', $line);
// 记录
$redis->zAdd($key, $time, uniqid('', true));
// 更新集合的 TTL
$redis->expire($key, $ttl);

后记

拿什么做key是一个需要三思的问题,对于一个固定的访问者用固定的方式访问,它必须也是固定的

比如,如果你设计了自己的 APIKEY 体系,切忌将 APIKEY 作为key,因为 APIKEY 可能是可变的(取决于你自己的设计),而比较妥善的方法是将 APIKEY 所属的用户的 ID 之类的作为key

搬瓦工VPS优惠套餐,建站稳如狗,支持支付宝,循环出账94折优惠码BWH3HYATVBJW
年付$47CN2线路,1核/1G内存/20G硬盘/1T@1Gbps【点击购买
季付$47CN2 GIA线路,1核/1G内存/20G硬盘/1T@2.5Gbps【点击购买
Last modification:May 4th, 2020 at 12:56 am
If you think my article is useful to you, please feel free to appreciate

Leave a Comment Cancel reply

7 comments

  1. 花火  Android 10(Android 10) / Google Chrome 99.0.4844.94(Google Chrome 99.0.4844.94)
    请问有没有什么比较好用的redis可视化管理工具推荐,最好上部署在服务器端的web页面,而非本地客户端
    1. 神代綺凛  Windows 10 x64 Edition(Windows 10 x64 Edition) / Google Chrome 99.0.4844.84(Google Chrome 99.0.4844.84)
      1. 花火  Android 10(Android 10) / Google Chrome 99.0.4844.94(Google Chrome 99.0.4844.94)
        @神代綺凛 我刚找到这个,就是css有点丑,还得自己美化
  2. 悠悠千反田  Mac OS X 10.13.6(Mac OS X 10.13.6) / Google Chrome 80.0.3987.116(Google Chrome 80.0.3987.116)
    大佬有兴趣的话可以看看限流算法,比如令牌桶这些,不过那个的目的是为了防止系统被冲垮,大佬你只是想限制某些 ip 或者用户而已~~
    1. 神代綺凜  Windows 10 x64 Edition(Windows 10 x64 Edition) / Google Chrome 83.0.4103.61(Google Chrome 83.0.4103.61)
      @悠悠千反田 是,目前访问量也还没到需要限流的程度
  3. KADA  Windows 7 x64 Edition(Windows 7 x64 Edition) / Google Chrome 80.0.3987.149(Google Chrome 80.0.3987.149)
    问大佬一个问题,我想实现从两个或多个文件内随机抽取一个链接应该怎么做,https://temp.kada.monster/t/code.JPG 现在是从bg.txt内抽取一个链接,如果我想从bg.txt&h.txt里抽取一个链接应该怎么办
    1. 神代綺凜  Mac OS X 10.15.3(Mac OS X 10.15.3) / Google Chrome 80.0.3987.149(Google Chrome 80.0.3987.149)
      @KADA 1、你这个问题和文章无关,理应到留言板提问
      2、那不就两个 txt 都读,然后拼成一个数组再随机就行