如何限制站点的 API 用户?

2022-08-30 10:11:15

我的网站的合法用户偶尔会用API请求来锤击服务器,从而导致不良结果。我想设定一个限制,即每5秒不超过一次API调用或每分钟n次调用(还没有弄清楚确切的限制)。显然,我可以记录数据库中的每个API调用,并对每个请求进行计算,以查看它们是否超过限制,但是每个请求上的所有这些额外开销都会破坏目的。我还可以使用哪些其他资源密集程度较低的方法来制定限制?我正在使用PHP / Apache / Linux,因为它是值得的。


答案 1

好吧,没有办法在不写入服务器的情况下执行我要求的事情,但我至少可以消除记录每个请求。一种方法是使用“泄漏桶”限制方法,其中它仅跟踪最后一个请求()以及时间范围内的请求数/限制的比率()。泄漏的存储桶永远不会重置其计数器(与 Twitter API 的限流不同,后者每小时重置一次),但如果存储桶已满(用户达到限制),他们必须等待几秒钟,以便存储桶清空一点,然后才能发出另一个请求。换句话说,这就像一个滚动限制:如果在时间范围内有以前的请求,它们就会慢慢从桶中泄漏出来;它只限制你,如果你填满桶。$last_api_request$minute_throttlen

此代码段将为每个请求计算一个新值。我指定了分钟,因为您可以为任何时间段添加限制,例如每小时,每天等...尽管不止一个会很快开始使用户感到困惑。$minute_throttle$minute_throttle

$minute = 60;
$minute_limit = 100; # users are limited to 100 requests/minute
$last_api_request = $this->get_last_api_request(); # get from the DB; in epoch seconds
$last_api_diff = time() - $last_api_request; # in seconds
$minute_throttle = $this->get_throttle_minute(); # get from the DB
if ( is_null( $minute_limit ) ) {
    $new_minute_throttle = 0;
} else {
    $new_minute_throttle = $minute_throttle - $last_api_diff;
    $new_minute_throttle = $new_minute_throttle < 0 ? 0 : $new_minute_throttle;
    $new_minute_throttle += $minute / $minute_limit;
    $minute_hits_remaining = floor( ( $minute - $new_minute_throttle ) * $minute_limit / $minute  );
    # can output this value with the request if desired:
    $minute_hits_remaining = $minute_hits_remaining >= 0 ? $minute_hits_remaining : 0;
}

if ( $new_minute_throttle > $minute ) {
    $wait = ceil( $new_minute_throttle - $minute );
    usleep( 250000 );
    throw new My_Exception ( 'The one-minute API limit of ' . $minute_limit 
        . ' requests has been exceeded. Please wait ' . $wait . ' seconds before attempting again.' );
}
# Save the values back to the database.
$this->save_last_api_request( time() );
$this->save_throttle_minute( $new_minute_throttle );

答案 2

您可以使用令牌存储桶算法控制速率,该算法与泄漏存储桶算法相当。请注意,您必须在进程(或您要控制的任何范围)上共享存储桶的状态(即令牌数量)。因此,您可能需要考虑锁定以避免争用条件。

好消息是:我为您完成了所有这些工作:带宽限制/令牌桶

use bandwidthThrottle\tokenBucket\Rate;
use bandwidthThrottle\tokenBucket\TokenBucket;
use bandwidthThrottle\tokenBucket\storage\FileStorage;

$storage = new FileStorage(__DIR__ . "/api.bucket");
$rate    = new Rate(10, Rate::SECOND);
$bucket  = new TokenBucket(10, $rate, $storage);
$bucket->bootstrap(10);

if (!$bucket->consume(1, $seconds)) {
    http_response_code(429);
    header(sprintf("Retry-After: %d", floor($seconds)));
    exit();
}

推荐