借助 redis 实现访问控制

1303 字
7 分钟
借助 redis 实现访问控制
借助 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

文章分享

如果这篇文章对你有帮助,欢迎分享给更多人!

借助 redis 实现访问控制
https://moe.best/posts/redis-access-control/
作者
神代綺凛
发布于
2020-03-16
许可协议
CC BY-NC-SA 4.0
Profile Image of the Author
神代綺凛
专业咕咕
分类
标签
站点统计
文章
107
分类
15
标签
269
总字数
140,468
运行时长
0 天
最后活动
0 天前
站点信息
构建平台
EdgeOne Pages
博客版本
Firefly v6.16.8
文章许可
CC BY-NC-SA 4.0
文章目录