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žiadavka | Odpoveď |
|---|---|
1. až 3. /quote s X-Api-Key: key-a | 200, RateLimit: "perKey";r=2, 1, 0 |
4. /quote s key-a | 429 s Retry-After |
1. /quote s key-b | 200, vlastná kvóta, r=2 |
/quote so zlým kľúčom | 401, počíta ho len perIp |
3. /ping | 429, Retry-After: 30 |
akýkoľvek /status | 200, 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.
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
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
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
#!/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?.