轻松做到基于 IP 或用户 ID 等的单位时间内访问频率的限制
Head Pic: #明日方舟 双狼 by 卧龙先生的爸爸 - pixiv
做开放 API,访问控制防止滥用是相当重要的一部分
我相信世上好人还是多,但不可避免地总会出现那么几个憨憨,普通 WAF 提供的基于 URL 的访问频率限制已经不顶用了,自己拓展又很麻烦
想了想,不如直接用开发 API 的语言写好了,方便、契合度还高
关于 redis
为什么用 redis
其实也没有什么特别的理由,只是碰巧想了解学习一下 redis,结果无意间发现 redis 很适合用来做访问记录,主要因为以下两点
- 支持有序集合 (sorted sets)
- 可为某个键设置 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 或用户 IDscore
- 时间戳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
版权声明:本文为原创文章,版权归 神代綺凜 所有。
本文链接:https://moe.best/gotagota/redis-access-control.html
所有原创文章采用 知识共享署名-非商业性使用 4.0 国际许可协议 进行许可。
您可以自由的转载和修改,但请务必注明文章来源并且不可用于商业目的。
2、那不就两个 txt 都读,然后拼成一个数组再随机就行