Ubuntu20.04でRedisを使用してPHPレート制限を実装する方法
著者は、 Apache Software Foundation を選択して、 Write forDOnationsプログラムの一環として寄付を受け取りました。
序章
Redis( Re mote Di ctionary S erver)は、メモリ内のオープンソースソフトウェアです。 これは、サーバーのRAMを使用するデータ構造ストアであり、最速のソリッドステートドライブ(SSD)よりも数倍高速です。 これにより、Redisの応答性が高くなるため、レート制限に適しています。
レート制限は、ユーザーがサーバーにリソースを要求できる回数に上限を設けるテクノロジーです。 多くのサービスは、ユーザーがサーバーに過度の負荷をかけようとしたときにサービスが悪用されるのを防ぐために、レート制限を実装しています。
たとえば、 PHP を使用してWebアプリケーションにパブリックAPI(アプリケーションプログラミングインターフェイス)を実装する場合、何らかの形式のレート制限が必要です。 その理由は、APIを公開するときに、アプリケーションユーザーが特定の時間枠でアクションを繰り返すことができる回数を制御したいからです。 制御がないと、ユーザーはシステムを完全に停止させる可能性があります。
特定の制限を超えるユーザーの要求を拒否すると、アプリケーションをスムーズに実行できます。 顧客が多い場合、レート制限により、各顧客がアプリケーションに高速でアクセスできるようにする公正な使用ポリシーが適用されます。 レート制限は、帯域幅のコストを削減し、サーバーの輻輳を最小限に抑えるのにも役立ちます。
MySQLのようなデータベースにユーザーアクティビティを記録することにより、レート制限モジュールをコーディングすることは実用的かもしれません。 ただし、データをディスクからフェッチして設定された制限と比較する必要があるため、多くのユーザーがシステムにアクセスする場合、最終製品はスケーラブルではない可能性があります。 これは遅いだけでなく、リレーショナルデータベース管理システムはこの目的のために設計されていません。
Redisはインメモリデータベースとして機能するため、レートリミッターを作成するための適格な候補であり、この目的で信頼できることが証明されています。
このチュートリアルでは、Ubuntu20.04サーバーでRedisを使用してレート制限するためのPHPスクリプトを実装します。
前提条件
始める前に、次のものが必要です。
-
Ubuntu20.04サーバーとsudo権限を持つroot以外のユーザー。 サーバーをセットアップして新しいユーザーを作成するには、 Ubuntu20.04を使用したサーバーの初期セットアップガイドを参照してください。
-
LAMPスタック。 Ubuntu 20.04 にLinux、Apache、MySQL、PHP(LAMP)スタックをインストールする方法に従ってください。 このガイドでは、ステップ4 — Webサイトの仮想ホストの作成をスキップして、Apacheのインストールですでに作成されているデフォルトの仮想ホストを使用できます。
-
Redisサーバー。 Ubuntu 20.04にRedisをインストールして保護する方法-クイックスタートチュートリアルに従って、これを設定します。
ステップ1—PHP用のRedisライブラリをインストールする
まず、Ubuntuサーバーパッケージリポジトリインデックスを更新することから始めます。 次に、php-redis
拡張機能をインストールします。 これは、PHPコードにRedisを実装できるようにするライブラリです。 これを行うには、次のコマンドを実行します。
- sudo apt update
- sudo apt install -y php-redis
次に、Apacheサーバーを再起動して、php-redis
ライブラリをロードします。
- sudo systemctl restart apache2
ソフトウェア情報インデックスを更新し、PHP用のRedisライブラリをインストールしたら、IPアドレスに基づいてユーザーのアクセスを制限するPHPリソースを作成します。
ステップ2—レート制限のためのPHPWebリソースの構築
この手順では、Webサーバーのルートディレクトリ(/var/www/html/
)にtest.php
ファイルを作成します。 このファイルは一般に公開され、ユーザーはWebブラウザにそのアドレスを入力して実行できます。 ただし、このガイドの基礎として、後でcurl
コマンドを使用してリソースへのアクセスをテストします。
サンプルリソースファイルを使用すると、ユーザーは10秒の時間枠で3回アクセスできます。 制限を超えようとすると、レート制限されていることを通知するエラーが発生します。
このファイルのコア機能は、Redisサーバーに大きく依存しています。 ユーザーが初めてリソースをリクエストすると、ファイル内のPHPコードは、ユーザーのIPアドレスに基づいてRedisサーバー上にキーを作成します。
ユーザーがリソースに再度アクセスすると、PHPコードはユーザーのIPアドレスをRedisサーバーに保存されているキーと照合し、キーが存在する場合は値を1つインクリメントしようとします。 PHPコードは、増分された値が設定された最大制限に達しているかどうかをチェックし続けます。
ユーザーのIPアドレスに基づくRedisキーは、10秒後に期限切れになります。 この期間が経過すると、ユーザーのWebリソースへのアクセスのログ記録が再開されます。
まず、/var/www/html/test.php
ファイルを開きます。
- sudo nano /var/www/html/test.php
次に、次の情報を入力してRedisクラスを初期化します。 REDIS_PASSWORD
に適切な値を入力することを忘れないでください。
<?php
$redis = new Redis();
$redis->connect('127.0.0.1', 6379);
$redis->auth('REDIS_PASSWORD');
$redis->auth
は、Redisサーバーへのプレーンテキスト認証を実装します。 これは、ローカルで(localhost
を介して)作業している間は問題ありませんが、リモートRedisサーバーを使用している場合は、SSL認証の使用を検討してください。
次に、同じファイルで、次の変数を初期化します。
. . .
$max_calls_limit = 3;
$time_period = 10;
$total_user_calls = 0;
あなたが定義した:
$max_calls_limit
:ユーザーがリソースにアクセスできる呼び出しの最大数です。$time_period
:$max_calls_limit
に従って、ユーザーがリソースにアクセスできる時間枠を秒単位で定義します。$total_user_calls
:指定された時間枠内にユーザーがリソースへのアクセスを要求した回数を取得する変数を初期化します。
次に、次のコードを追加して、Webリソースを要求しているユーザーのIPアドレスを取得します。
. . .
if (!empty($_SERVER['HTTP_CLIENT_IP'])) {
$user_ip_address = $_SERVER['HTTP_CLIENT_IP'];
} elseif (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
$user_ip_address = $_SERVER['HTTP_X_FORWARDED_FOR'];
} else {
$user_ip_address = $_SERVER['REMOTE_ADDR'];
}
このコードはデモンストレーションの目的でユーザーのIPアドレスを使用しますが、認証が必要な保護されたリソースがサーバー上にある場合は、ユーザー名またはアクセストークンを使用してユーザーのアクティビティをログに記録できます。
このようなシナリオでは、システムに認証されたすべてのユーザーが一意の識別子(たとえば、顧客ID、開発者ID、ベンダーID、さらにはユーザーID)を持ちます。 (これを構成する場合は、$user_ip_address
の代わりにこれらの識別子を使用することを忘れないでください。)
このガイドでは、ユーザーIPアドレスで概念を証明できます。 したがって、前のコードスニペットでユーザーのIPアドレスを取得したら、次のコードブロックをファイルに追加します。
. . .
if (!$redis->exists($user_ip_address)) {
$redis->set($user_ip_address, 1);
$redis->expire($user_ip_address, $time_period);
$total_user_calls = 1;
} else {
$redis->INCR($user_ip_address);
$total_user_calls = $redis->get($user_ip_address);
if ($total_user_calls > $max_calls_limit) {
echo "User " . $user_ip_address . " limit exceeded.";
exit();
}
}
echo "Welcome " . $user_ip_address . " total calls made " . $total_user_calls . " in " . $time_period . " seconds";
このコードでは、if...else
ステートメントを使用して、RedisサーバーにIPアドレスで定義されたキーがあるかどうかを確認します。 キーif (!$redis->exists($user_ip_address)) {...}
が存在しない場合は、キーを設定し、コード$redis->set($user_ip_address, 1);
を使用してその値を1
に定義します。
$redis->expire($user_ip_address, $time_period);
は、期間内(この場合は10
秒)に有効期限が切れるキーを設定します。
ユーザーのIPアドレスがRedisキーとして存在しない場合は、変数$total_user_calls
を1
に設定します。
...else {...}...
ステートメントブロックで、$redis->INCR($user_ip_address);
コマンドを使用して、各IPアドレスキーに設定されたRedisキーの値を1
ずつインクリメントします。 これは、キーがRedisサーバーにすでに設定されており、リピートリクエストとしてカウントされる場合にのみ発生します。
ステートメント$total_user_calls = $redis->get($user_ip_address);
は、RedisサーバーでIPアドレスベースのキーをチェックすることにより、ユーザーが行うリクエストの総数を取得します。
ファイルの終わりに向かって、...if ($total_user_calls > $max_calls_limit) {... }..
ステートメントを使用して、制限を超えているかどうかを確認します。 その場合は、echo "User " . $user_ip_address . " limit exceeded.";
でユーザーに警告します。 最後に、echo "Welcome " . $user_ip_address . " total calls made " . $total_user_calls . " in " . $time_period . " seconds";
ステートメントを使用して、その期間に行われた訪問についてユーザーに通知します。
すべてのコードを追加すると、/var/www/html/test.php
ファイルは次のようになります。
<?php
$redis = new Redis();
$redis->connect('127.0.0.1', 6379);
$redis->auth('REDIS_PASSWORD');
$max_calls_limit = 3;
$time_period = 10;
$total_user_calls = 0;
if (!empty($_SERVER['HTTP_CLIENT_IP'])) {
$user_ip_address = $_SERVER['HTTP_CLIENT_IP'];
} elseif (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
$user_ip_address = $_SERVER['HTTP_X_FORWARDED_FOR'];
} else {
$user_ip_address = $_SERVER['REMOTE_ADDR'];
}
if (!$redis->exists($user_ip_address)) {
$redis->set($user_ip_address, 1);
$redis->expire($user_ip_address, $time_period);
$total_user_calls = 1;
} else {
$redis->INCR($user_ip_address);
$total_user_calls = $redis->get($user_ip_address);
if ($total_user_calls > $max_calls_limit) {
echo "User " . $user_ip_address . " limit exceeded.";
exit();
}
}
echo "Welcome " . $user_ip_address . " total calls made " . $total_user_calls . " in " . $time_period . " seconds";
/var/www/html/test.php
ファイルの編集が終了したら、保存して閉じます。
これで、test.php
Webリソースのユーザーをレート制限するために必要なロジックがコーディングされました。 次のステップでは、スクリプトをテストします。
ステップ3—Redisレート制限のテスト
このステップでは、curl
コマンドを使用して、ステップ2でコーディングしたWebリソースを要求します。 スクリプトを完全にチェックするには、1つのコマンドで5回リソースをリクエストします。 これを行うには、test.php
ファイルの最後にプレースホルダーURLパラメーターを含めます。 ここでは、リクエストの最後に値?[1-5]
を使用して、curl
コマンドを5回実行します。
次のコマンドを実行します。
- curl -H "Accept: text/plain" -H "Content-Type: text/plain" -X GET http://localhost/test.php?[1-5]
コードを実行すると、次のような出力が表示されます。
Output[1/5]: http://localhost/test.php?1 --> <stdout>
--_curl_--http://localhost/test.php?1
Welcome 127.0.0.1 total calls made 1 in 10 seconds
[2/5]: http://localhost/test.php?2 --> <stdout>
--_curl_--http://localhost/test.php?2
Welcome 127.0.0.1 total calls made 2 in 10 seconds
[3/5]: http://localhost/test.php?3 --> <stdout>
--_curl_--http://localhost/test.php?3
Welcome 127.0.0.1 total calls made 3 in 10 seconds
[4/5]: http://localhost/test.php?4 --> <stdout>
--_curl_--http://localhost/test.php?4
User 127.0.0.1 limit exceeded.
[5/5]: http://localhost/test.php?5 --> <stdout>
--_curl_--http://localhost/test.php?5
User 127.0.0.1 limit exceeded.
お気づきのとおり、最初の3つのリクエストは問題なく実行されました。 ただし、スクリプトでは4番目と5番目のリクエストのレートが制限されています。 これは、Redisサーバーがユーザーのリクエストをレート制限していることを確認します。
このガイドでは、次の2つの変数に低い値を設定しました。
...
$max_calls_limit = 3;
$time_period = 10;
...
実稼働環境でアプリケーションを設計する場合、ユーザーがアプリケーションにアクセスする頻度に応じて、より高い値を検討できます。
これらの値を設定する前に、リアルタイムの統計を確認することをお勧めします。 たとえば、サーバーログに、平均的なユーザーが60秒ごとに1,000回アプリケーションにアクセスしたことが示されている場合は、それらの値をユーザーを制限するためのベンチマークとして使用できます。
より良い視点で物事を置くために、ここにレート制限の実装のいくつかの実際の例があります(2021年現在):
- Twitterは、
/statuses/mentions_timeline
および/statuses/user_timeline
エンドポイントに対して1日あたり100,000件のリクエストのみを許可します。 - DigitalOceanは、OAuthトークンで認証されたユーザーごとに、APIエンドポイントで1時間あたり5,000リクエストを許可します。
- Googleカスタム検索JSONAPIでは、1日あたり100件の検索クエリを無料で利用できます。
結論
このチュートリアルでは、Ubuntu 20.04サーバーでRedisを使用してレート制限するためのPHPスクリプトを実装し、Webアプリケーションが不注意または悪意のある乱用から保護されるようにしました。 ユースケースに応じて、ニーズに合わせてコードを拡張できます。
本番環境で使用するためにApacheサーバーを保護することをお勧めします。 Ubuntu20.04でLet’sEncryptを使用してApacheを保護する方法チュートリアルに従ってください。
また、Redisがデータベースキャッシュとしてどのように機能するかを読むことも検討してください。 Ubuntu20.04チュートリアルでPHPを使用してMySQLのキャッシュとしてRedisを設定する方法を試してください。