Preskočiť na obsah

Príklad: obmedzenie požiadaviek ​

Každý endpoint povolí 100 požiadaviek za minútu z jednej IP. GET /api/v1/quote navyše povolí tri požiadavky za hodinu na API kľúč, GET /api/v1/ping dve za minútu podľa svojho atribútu a GET /api/v1/status nemá limit.

PožiadavkaOdpoveď
1. až 3. /quote s X-Api-Key: key-a200, RateLimit: "perKey";r=2, 1, 0
4. /quote s key-a429 s Retry-After
1. /quote s key-b200, vlastná kvóta, r=2
/quote so zlým kľúčom401, počíta ho len perIp
3. /ping429, Retry-After: 30
akýkoľvek /status200, bez hlavičiek obmedzenia

Konfigurácia ​

rateLimits definuje pomenované politiky, defaults.rateLimit použije perIp na každý endpoint, quote pridá perKey a status limit vypne.

yaml
extensions:
	api: Tomaj\NetteApi\DI\NetteApiExtension

parameters:
	apiKeys:
		key-a: '*'
		key-b: '*'

services:
	router: App\RouterFactory::create

api:
	# where the counters are kept: cache (the default, the Nette cache in %tempDir%), memory, redis or your own storage
	rateLimiting:
		storage: cache

	authorizations:
		apiKey: Tomaj\NetteApi\Authorization\HeaderApiKeyAuthentication('X-Api-Key', Tomaj\NetteApi\Misc\StaticTokenRepository(%apiKeys%))

	# named policies, shared by every endpoint that uses the name
	rateLimits:
		# checked before the authorization, so a client guessing keys is limited too
		perIp: {algorithm: slidingWindow, limit: 100, interval: 1 minute, key: ip}
		# checked after the authorization, one quota per API key
		perKey: {algorithm: fixedWindow, limit: 3, interval: 1 hour, key: identity}

	# the app-wide default for every endpoint
	defaults:
		authorization: apiKey
		rateLimit: perIp

	# the endpoints with #[RateLimit] on the class
	discovery:
		paths: [%appDir%/Api/Discovered]

	endpoints:
		# override: both policies count
		- {method: GET, version: 1, package: quote, handler: App\Api\QuoteHandler, rateLimit: [perIp, perKey]}
		# disabled: a public health check without any limit
		- {method: GET, version: 1, package: status, handler: App\Api\StatusHandler, authorization: false, rateLimit: false}

Handlery ​

php
<?php

declare(strict_types=1);

namespace App\Api;

use Tomaj\NetteApi\Handlers\BaseHandler;
use Tomaj\NetteApi\Response\JsonApiResponse;
use Tomaj\NetteApi\Response\ResponseInterface;

final class QuoteHandler extends BaseHandler
{
    public function summary(): string
    {
        return 'A quote, at most three per hour per API key and 100 per minute per IP';
    }

    public function handle(array $params): ResponseInterface
    {
        return new JsonApiResponse(200, ['quote' => 'Simplicity is prerequisite for reliability.']);
    }
}

#[RateLimit] na objavenom handleri nahradí predvolený limit vlastným:

php
<?php

declare(strict_types=1);

namespace App\Api\Discovered;

use Tomaj\NetteApi\Attribute\Authorization;
use Tomaj\NetteApi\Attribute\Get;
use Tomaj\NetteApi\Attribute\RateLimit;
use Tomaj\NetteApi\Handlers\BaseHandler;
use Tomaj\NetteApi\Response\JsonApiResponse;
use Tomaj\NetteApi\Response\ResponseInterface;

/**
 * An inline limit on the class instead of the default perIp: 2 requests per minute per IP, with a token bucket
 */
#[Get(1, 'ping')]
#[Authorization(false)]
#[RateLimit(limit: 2, interval: '1 minute', algorithm: 'tokenBucket', name: 'ping')]
final class PingHandler extends BaseHandler
{
    public function summary(): string
    {
        return 'Pong, at most 2 per minute per IP';
    }

    public function handle(array $params): ResponseInterface
    {
        return new JsonApiResponse(200, ['pong' => true]);
    }
}

Test ​

bash
#!/usr/bin/env bash
# End-to-end check: an app-wide default limit, an endpoint with two policies, an attribute limit and an endpoint without a limit.
set -euo pipefail
cd "$(dirname "$0")"
[[ -f vendor/autoload.php ]] || { echo "fail: vendor/ is missing, run composer install first" >&2; exit 1; }
source ../ci/lib.sh
rm -rf temp/cache
serve 8095
avoid_minute_edge

for remaining in 2 1 0; do
    request -H 'X-Api-Key: key-a' "$BASE/api/v1/quote"
    expect_status 200 "request with $remaining left"
    expect_header 'RateLimit-Policy: "perIp";q=100;w=60' "perIp policy"
    expect_header 'RateLimit-Policy: "perKey";q=3;w=3600' "perKey policy"
    expect_header "RateLimit: \"perKey\";r=$remaining;" "perKey remaining"
done
ok "GET /quote counts perIp and perKey, RateLimit counts down"

request -H 'X-Api-Key: key-a' "$BASE/api/v1/quote"
expect_status 429 "fourth"
expect_header 'RateLimit: "perKey";r=0;' "fourth"
expect_header 'Retry-After:' "fourth"
expect_body '"message":"Too many requests. Retry after' "fourth"
ok "the fourth request of a key answers 429 with Retry-After"

request -H 'X-Api-Key: key-b' "$BASE/api/v1/quote"
expect_status 200 "other key"
expect_header 'RateLimit: "perKey";r=2;' "other key"
ok "another key has its own quota"

request -H 'X-Api-Key: wrong' "$BASE/api/v1/quote"
expect_status 401 "wrong key"
expect_header 'RateLimit: "perIp";r=94;' "wrong key"
no_header 'RateLimit-Policy: "perKey"' "wrong key"
ok "a wrong key is counted by perIp before the authorization, perKey needs an identity"

for i in 1 2; do
    request "$BASE/api/v1/ping"
    expect_status 200 "ping $i"
    expect_header 'RateLimit-Policy: "ping";q=2;w=60' "ping"
done
request "$BASE/api/v1/ping"
expect_status 429 "third ping"
expect_header 'Retry-After: ' "third ping"
ok "#[RateLimit(limit: 2, ...)] replaces the default, the third ping has to wait"

for i in 1 2 3 4 5; do
    request "$BASE/api/v1/status"
    expect_status 200 "status $i"
    no_header 'RateLimit' "status"
done
ok "rateLimit: false turns the default off"

Časté otázky ​

Prečo sa počíta aj zlý kľúč? ​

perIp má kľúč podľa IP, takže beží pred autorizáciou a obmedzí aj klienta, ktorý háda kľúče. perKey má kľúč identity, takže beží až po úspešnej autorizácii. Pozrite si Obmedzenie požiadaviek.

Prečo tretí ping čaká 30 sekúnd? ​

tokenBucket s 2 za minútu doplní jeden token každých 30 sekúnd.

Kde sú počítadlá? ​

V Nette cache v temp/cache. Pri viacerých serveroch použite storage: redis, pozrite si Úložiská.

Aké hlavičky klient dostane? ​

RateLimit-Policy a RateLimit pre každú použitú politiku a Retry-After pri 429.

Môžem obmedzovať podľa IP za proxy? ​

Áno, uveďte proxy v http: proxy:, inak všetci klienti zdieľajú jej kvótu. Pozrite si Ktorá IP sa počíta?.

Vydané pod licenciou MIT.