AppPlatformでNode.jsを使用してレートリミッターを構築する方法
序章
レート制限は、ネットワークのトラフィックを管理し、APIの使用など、特定の期間に誰かが操作を繰り返す回数を制限します。 レート制限の乱用に対するセキュリティの層がないサービスは、過負荷になりがちであり、正当な顧客に対するアプリケーションの適切な動作を妨げます。
このチュートリアルでは、リクエストのIPアドレスを確認し、ユーザーごとのリクエストのタイムスタンプを比較してこれらのリクエストの割合を計算するNode.jsサーバーを構築します。 IPアドレスがアプリケーションに設定した制限を超える場合は、CloudflareのAPIを呼び出し、IPアドレスをリストに追加します。 次に、リストにIPアドレスを持つすべてのリクエストを禁止するCloudflareファイアウォールルールを構成します。
このチュートリアルの終わりまでに、DigitalOceanの App Platform にデプロイされたNode.jsプロジェクトを構築し、レート制限でCloudflareルーティングドメインを保護します。
前提条件
このガイドを開始する前に、次のものが必要です。
- Cloudflareアカウント。 チュートリアルにはCloudflareの無料プランで十分です。 新しいアカウントを作成する場合は、無料プランを選択してください。 Cloudflareアカウントの作成とウェブサイトの追加に関するこのガイドは、あなたを助けることができます。
- Cloudflareアカウントに追加された登録済みドメイン。 Cloudflare を使用してWebサイトに対するDDoS攻撃を軽減する方法に関するガイドは、これを設定するのに役立ちます。 DNSの用語、コンポーネント、および概念の概要に関するこの記事も役立ちます。
- Node.jsを使用したBasicExpressサーバー。 ステップ2までのNode.jsとExpressの使用を開始する方法の記事に従ってください。
- ローカルマシンにGitHubアカウントとgitがインストールされています。 コードをGitHubにプッシュしてDigitalOceanAppPlatformからデプロイするため、GitHubアカウントとgitがインストールされている必要があります。
- DigitalOceanアカウント。
ステップ1— Node.jsプロジェクトをセットアップし、DigitalOceanのアプリプラットフォームにデプロイする
このステップでは、基本的なExpressサーバーを拡張し、コードをGitHubリポジトリにプッシュして、アプリケーションをAppPlatformにデプロイします。
コードエディタを使用して、基本的なExpressサーバーのプロジェクトディレクトリを開きます。 名前で新しいファイルを作成します .gitignore
プロジェクトのルートディレクトリにあります。 新しく作成した行に次の行を追加します .gitignore
ファイル:
node_modules/
.env
あなたの最初の行 .gitignore
fileは、gitが追跡しないようにするためのディレクティブです。 node_modules
ディレクトリ。 これにより、リポジトリのサイズを小さく保つことができます。 The node_modules
コマンドを実行することにより、必要に応じて生成できます npm install
. 2行目は、環境変数ファイルが追跡されないようにします。 を作成します .env
さらなるステップでファイルします。
に移動します server.js
コードエディタで、次のコード行を変更します。
...
app.listen(process.env.PORT || 3000, () => {
console.log(`Example app is listening on port ${process.env.PORT || 3000}`);
});
条件付き使用への変更 PORT
環境変数として、アプリケーションは割り当てられた上でサーバーを動的に実行できます PORT
または使用する 3000
フォールバックとして。
注:の文字列 console.log()
引用符ではなく、backticks( `)で囲まれています。 これにより、テンプレートリテラルを使用できるようになります。これにより、文字列内に式を含めることができます。
ターミナルウィンドウにアクセスして、アプリケーションを実行します。
- node server.js
ブラウザウィンドウが表示されます Successful response
. ターミナルに、次の出力が表示されます。
OutputExample app is listening on port 3000
Expressサーバーが正常に実行されたら、AppPlatformにデプロイします。
まず、初期化します git
プロジェクトのルートディレクトリにあり、コードをGitHubアカウントにプッシュします。 ブラウザでAppPlatformダッシュボードに移動し、 CreateAppボタンをクリックします。 GitHub オプションを選択し、必要に応じてGitHubで承認します。 AppPlatformにデプロイするプロジェクトのドロップダウンリストからプロジェクトのリポジトリを選択します。 構成を確認してから、アプリケーションに名前を付けます。 このチュートリアルでは、アプリケーションの開発フェーズで作業するため、Basicプランを選択します。 準備ができたら、アプリの起動をクリックします。
次に、設定タブに移動し、ドメインセクションをクリックします。 Cloudflare経由でルーティングされたドメインをドメインまたはサブドメイン名フィールドに追加します。 箇条書きを選択します。ドメインを管理して、ドメインのCloudflareDNSアカウントに追加するために使用するCNAMEレコードをコピーします。
アプリケーションをAppPlatformにデプロイしたら、後でApp Platformのダッシュボードに戻るので、新しいタブでCloudflareのドメインのダッシュボードに移動します。 DNSタブに移動します。 レコードの追加ボタンをクリックし、タイプとして CNAME 、ルートとして @ を選択し、 CNAME
AppPlatformからコピーしました。 保存ボタンをクリックし、AppPlatformのダッシュボードの設定タブの下にあるドメインセクションに移動し、ドメインの追加をクリックします。 ] ボタン。
展開タブをクリックして、展開の詳細を確認します。 展開が完了したら、開くことができます your_domain
ブラウザで表示します。 ブラウザウィンドウに次のように表示されます。 Successful response
. AppPlatformダッシュボードのRuntimeLogs タブに移動すると、次の出力が表示されます。
OutputExample app is listening on port 8080
注:ポート番号 8080
AppPlatformによってデフォルトで割り当てられたポートです。 展開前にアプリを確認しながら構成を変更することで、これをオーバーライドできます。
アプリケーションがAppPlatformにデプロイされたので、レートリミッターへのリクエストを計算するためにキャッシュの概要を説明する方法を見てみましょう。
ステップ2—ユーザーのIPアドレスをキャッシュして1秒あたりのリクエスト数を計算する
このステップでは、ユーザーのIPアドレスをタイムスタンプの配列とともに cache に保存し、各ユーザーのIPアドレスの1秒あたりのリクエスト数を監視します。 キャッシュは、アプリケーションで頻繁に使用されるデータの一時的なストレージです。 キャッシュ内のデータは通常、RAM(ランダムアクセスメモリ)などのクイックアクセスハードウェアに保持されます。 キャッシュの基本的な目標は、キャッシュの下にある低速のストレージレイヤーにアクセスする必要性を減らすことで、データ取得のパフォーマンスを向上させることです。 3つのnpmパッケージを使用します。 node-cache
, is-ip
、 と request-ip
プロセスを支援します。
The request-ip
パッケージは、サーバーの要求に使用されるユーザーのIPアドレスをキャプチャします。 The node-cache
パッケージは、ユーザーの要求を追跡するために使用するメモリ内キャッシュを作成します。 を使用します is-ip
IPアドレスがIPv6アドレスであるかどうかを確認するために使用されるパッケージ。 をインストールします node-cache
, is-ip
、 と request-ip
ターミナルのnpm経由でパッケージ化します。
- npm i node-cache is-ip request-ip
を開きます server.js
コードエディタでファイルを作成し、以下のコード行を追加します const express = require('express');
:
...
const requestIP = require('request-ip');
const nodeCache = require('node-cache');
const isIp = require('is-ip');
...
ここの最初の行は requestIP
からのモジュール request-ip
インストールしたパッケージ。 このモジュールは、サーバーの要求に使用されるユーザーのIPアドレスをキャプチャします。 2行目は nodeCache
からのモジュール node-cache
パッケージ。 nodeCache
1秒あたりのユーザーのリクエストを追跡するために使用するメモリ内キャッシュを作成します。 3行目は isIp
からのモジュール is-ip
パッケージ。 これは、IPアドレスがIPv6であるかどうかをチェックします。これは、Cloudflareの仕様に従ってCIDR表記を使用するようにフォーマットします。
定数変数のセットを定義します server.js
ファイル。 これらの定数は、アプリケーション全体で使用します。
...
const TIME_FRAME_IN_S = 10;
const TIME_FRAME_IN_MS = TIME_FRAME_IN_S * 1000;
const MS_TO_S = 1 / 1000;
const RPS_LIMIT = 2;
...
TIME_FRAME_IN_S
アプリケーションがユーザーのタイムスタンプを平均化する期間を決定する定数変数です。 期間を長くすると、キャッシュサイズが大きくなるため、より多くのメモリを消費します。 The TIME_FRAME_IN_MS
定数変数は、アプリケーションがユーザーのタイムスタンプを平均化する期間も決定しますが、ミリ秒単位です。 MS_TO_S
ミリ秒単位の時間を秒に変換するために使用する変換係数です。 The RPS_LIMIT
variableは、レートリミッターをトリガーし、アプリケーションの要件に従って値を変更するアプリケーションのしきい値制限です。 値 2
の中に RPS_LIMIT
変数は、開発フェーズ中にトリガーされる中程度の値です。
Expressを使用すると、サーバーに送信されるすべてのHTTPリクエストにアクセスできるミドルウェア関数を記述して使用できます。 ミドルウェア関数を定義するには、 app.use()
そしてそれに関数を渡します。 名前の付いた関数を作成します ipMiddleware
ミドルウェアとして。
...
const ipMiddleware = async function (req, res, next) {
let clientIP = requestIP.getClientIp(req);
if (isIp.v6(clientIP)) {
clientIP = clientIP.split(':').splice(0, 4).join(':') + '::/64';
}
next();
};
app.use(ipMiddleware);
...
The getClientIp()
によって提供される機能 requestIP
リクエストオブジェクトを受け取り、 req
ミドルウェアから、パラメータとして。 The .v6()
機能はから来ます is-ip
モジュールとリターン true
渡された引数がIPv6アドレスの場合。 CloudflareのリストにはIPv6アドレスが必要です /64
CIDR表記。 次の形式に従うようにIPv6アドレスをフォーマットする必要があります。 aaaa:bbbb:cccc:dddd::/64
. .split(’:’)メソッドは、IPアドレスを含む文字列から配列を作成し、文字で分割します :
. .splice(0,4)メソッドは、配列の最初の4つの要素を返します。 The .join(':')
メソッドは、文字と組み合わせた配列から文字列を返します :
.
The next()
callは、ミドルウェアが存在する場合、次のミドルウェア関数に移動するようにミドルウェアに指示します。 あなたの例では、それはGETルートへのリクエストを受け取ります /
. これは、関数の最後に含めることが重要です。 そうしないと、リクエストはミドルウェアから転送されません。
のインスタンスを初期化します node-cache
定数の下に次の変数を追加します。
...
const IPCache = new nodeCache({ stdTTL: TIME_FRAME_IN_S, deleteOnExpire: false, checkperiod: TIME_FRAME_IN_S });
...
定数変数を使用 IPCache
、ネイティブのデフォルトパラメータをオーバーライドしています nodeCache
カスタムプロパティを使用:
stdTTL
:キャッシュ要素のキーと値のペアがキャッシュから削除されるまでの秒単位の間隔。TTL
Time To Live の略で、キャッシュが期限切れになるまでの時間の尺度です。deleteOnExpire
: に設定false
カスタムコールバック関数を記述して、expired
イベント。checkperiod
:期限切れの要素の自動チェックがトリガーされるまでの秒単位の間隔。 デフォルト値は600
、およびアプリケーションの要素の有効期限が小さい値に設定されているため、有効期限のチェックもより早く行われます。
のデフォルトパラメータの詳細については node-cache
、node-cachenpmパッケージのドキュメントページが便利です。 次の図は、キャッシュがデータを格納する方法を視覚化するのに役立ちます。
ここで、新しいIPアドレスの新しいキーと値のペアを作成し、IPアドレスがキャッシュに存在する場合は、既存のキーと値のペアに追加します。 値は、アプリケーションに対して行われた各要求に対応するタイムスタンプの配列です。 あなたの中で server.js
ファイル、作成 updateCache()
下の機能 IPCache
キャッシュへのリクエストのタイムスタンプを追加する定数変数:
...
const updateCache = (ip) => {
let IPArray = IPCache.get(ip) || [];
IPArray.push(new Date());
IPCache.set(ip, IPArray, (IPCache.getTtl(ip) - Date.now()) * MS_TO_S || TIME_FRAME_IN_S);
};
...
関数の最初の行は、指定されたIPアドレスのタイムスタンプの配列を取得します。nullの場合は、空の配列で初期化します。 次の行では、によってキャッチされた現在のタイムスタンプをプッシュしています new Date()
配列に機能します。 The .set()
によって提供される機能 node-cache
3つの引数を取ります: key
, value
そしてその TTL
. これ TTL
の値を置き換えることにより、標準のTTLセットを上書きします stdTTL
から IPCache
変数。 IPアドレスがすでにキャッシュに存在する場合は、既存のTTLを使用します。 それ以外の場合は、TTLを次のように設定します TIME_FRAME_IN_S
.
現在のキーと値のペアのTTLは、有効期限のタイムスタンプから現在のタイムスタンプを差し引くことによって計算されます。 次に、差は秒に変換され、3番目の引数として .set()
関数。 。getTtl()
関数は、キーとIPアドレスを引数として受け取り、キーと値のペアのTTLをタイムスタンプとして返します。 IPアドレスがキャッシュに存在しない場合は、 undefined
のフォールバック値を使用します TIME_FRAME_IN_S
.
注: JavaScriptはミリ秒単位で保存するため、ミリ秒から秒への変換タイムスタンプが必要です。 node-cache
モジュールは秒を使用します。
の中に ipMiddleware
ミドルウェアの場合、次の行を追加します if
コードブロック if (isIp.v6(clientIP))
アプリケーションを呼び出すIPアドレスの1秒あたりのリクエスト数を計算するには:
...
updateCache(clientIP);
const IPArray = IPCache.get(clientIP);
if (IPArray.length > 1) {
const rps = IPArray.length / ((IPArray[IPArray.length - 1] - IPArray[0]) * MS_TO_S);
if (rps > RPS_LIMIT) {
console.log('You are hitting limit', clientIP);
}
}
...
最初の行は、IPアドレスによって行われた要求のタイムスタンプを呼び出してキャッシュに追加します。 updateCache()
宣言した関数。 2行目は、IPアドレスのタイムスタンプの配列を収集します。 タイムスタンプの配列内の要素の数が1より大きく(1秒あたりのリクエストの計算には最低2つのタイムスタンプが必要)、1秒あたりのリクエストが定数で定義したしきい値を超える場合は、次のようになります。 console.log
IPアドレス。 The rps
変数は、リクエスト数を時間間隔の差で割って1秒あたりのリクエスト数を計算し、単位を秒に変換します。
プロパティをデフォルトにしたので deleteOnExpire
値に false
の中に IPCache
変数の場合、処理する必要があります expired
イベントを手動で。 node-cache
でトリガーするコールバック関数を提供します expired
イベント。 次のコード行を下に追加します IPCache
定数変数:
...
IPCache.on('expired', (key, value) => {
if (new Date() - value[value.length - 1] > TIME_FRAME_IN_MS) {
IPCache.del(key);
}
});
...
.on()
を受け入れるコールバック関数です key
と value
引数として期限切れの要素の。 キャッシュ内で、 value
リクエストのタイムスタンプの配列です。 強調表示された行は、配列の最後の要素が少なくともあるかどうかをチェックします TIME_FRAME_IN_S
現在より過去に。 タイムスタンプの配列に要素を追加しているときに、 value
少なくとも TIME_FRAME_IN_S
現在より過去に、 .del()
関数がかかります key
引数として、期限切れの要素をキャッシュから削除します。
配列の一部の要素が少なくとも TIME_FRAME_IN_S
現在より過去の場合は、期限切れのアイテムをキャッシュから削除して処理する必要があります。 次のコードをコールバック関数に追加します。 if
コードブロック if (new Date() - value[value.length - 1] > TIME_FRAME_IN_MS)
.
...
else {
const updatedValue = value.filter(function (element) {
return new Date() - element < TIME_FRAME_IN_MS;
});
IPCache.set(key, updatedValue, TIME_FRAME_IN_S - (new Date() - updatedValue[0]) * MS_TO_S);
}
...
JavaScriptにネイティブなfilter()配列メソッドは、タイムスタンプの配列内の要素をフィルター処理するためのコールバック関数を提供します。 あなたの場合、強調表示された行は、最も少ない要素をチェックします TIME_FRAME_IN_S
現在より過去に。 フィルタリングされた要素は、 updatedValue
変数。 これにより、フィルタリングされた要素でキャッシュが更新されます。 updatedValue
変数と新しいTTL。 の最初の要素に一致するTTL updatedValue
変数はトリガーします .on('expired')
キャッシュが次の要素を削除したときのコールバック関数。 の違い TIME_FRAME_IN_S
の最初のリクエストのタイムスタンプからの有効期限 updatedValue
新規および更新されたTTLを計算します。
ミドルウェア機能が定義されたら、ターミナルウィンドウにアクセスして、アプリケーションを実行します。
- node server.js
次に、 localhost:3000
Webブラウザで。 ブラウザウィンドウに次のように表示されます。 Successful response
. ページを繰り返し更新して、 RPS_LIMIT
. ターミナルウィンドウに次のように表示されます。
OutputExample app is listening on port 3000
You are hitting limit ::1
注:ローカルホストのIPアドレスは次のように表示されます。 ::1
. アプリケーションは、ローカルホストの外部にデプロイされたときにユーザーのパブリックIPをキャプチャします。
これで、アプリケーションはユーザーの要求を追跡し、タイムスタンプをキャッシュに保存できるようになります。 次のステップでは、CloudflareのAPIを統合してファイアウォールを設定します。
ステップ3—Cloudflareファイアウォールを設定する
このステップでは、レート制限に達したときにIPアドレスをブロックし、環境変数を作成し、CloudflareAPIを呼び出すようにCloudflareのファイアウォールを設定します。
ブラウザでCloudflareダッシュボードにアクセスし、ログインして、アカウントのホームページに移動します。 構成タブでリストを開きます。 で新しいリストを作成する your_list
名前として。
注: リストセクションは、Cloudflareドメインのダッシュボードページではなく、Cloudflareアカウントのダッシュボードページで利用できます。
ホームタブに移動して開きます your_domain
のダッシュボード。 ファイアウォールタブを開き、ファイアウォールルールセクションの下にあるファイアウォールルールの作成をクリックします。 与える your_rule_name
それを識別するためにファイアウォールに。 フィールドで、 IP Source Address
ドロップダウンから、 is in list
Operator の場合、および your_list
値の場合。 アクションの選択のドロップダウンで、ブロックを選択し、展開をクリックします。
作成する .env
アプリケーションからCloudflareAPIを呼び出すために、次の行を含むプロジェクトのルートディレクトリにファイルします。
ACCOUNT_MAIL=your_cloudflare_login_mail
API_KEY=your_api_key
ACCOUNT_ID=your_account_id
LIST_ID=your_list_id
の値を取得するには API_KEY
、CloudflareダッシュボードのマイプロファイルセクションのAPIトークンタブに移動します。 [グローバルAPIキー]セクションの表示をクリックし、Cloudflareパスワードを入力して表示します。 アカウントのホームページの構成タブの下にあるリストセクションにアクセスします。 横にある編集をクリックします your_list
作成したリスト。 取得する ACCOUNT_ID
と LIST_ID
のURLから your_list
ブラウザで。 URLは次の形式です。 https://dash.cloudflare.com/your_account_id/configurations/lists/your_list_id
警告:の内容を確認してください .env
機密が保持され、公開されません。 あなたが持っていることを確認してください .env
にリストされているファイル .gitignore
手順1で作成したファイル。 <$>
axiosをインストールして dotenv
ターミナルのnpm経由でパッケージ化します。
- npm i axios dotenv
を開きます server.js
コードエディタでファイルを作成し、その下に次のコード行を追加します。 nodeCache
定数変数:
...
const axios = require('axios');
require('dotenv').config();
...
ここの最初の行は axios
からのモジュール axios
インストールしたパッケージ。 このモジュールを使用して、CloudflareのAPIへのネットワーク呼び出しを行います。 2行目は、 dotenv
を有効にするモジュール process.env
に配置した値を定義するグローバル変数 .env
にファイルする server.js
.
以下をに追加します if (rps > RPS_LIMIT)
内の状態 ipMiddleware
その上 console.log('You are hitting limit', clientIP)
CloudflareAPIを呼び出します。
...
const url = `https://api.cloudflare.com/client/v4/accounts/${process.env.ACCOUNT_ID}/rules/lists/${process.env.LIST_ID}/items`;
const body = [{ ip: clientIP, comment: 'your_comment' }];
const headers = {
'X-Auth-Email': process.env.ACCOUNT_MAIL,
'X-Auth-Key': process.env.API_KEY,
'Content-Type': 'application/json',
};
try {
await axios.post(url, body, { headers });
} catch (error) {
console.log(error);
}
...
これで、URLを介してCloudflare APIを呼び出し、アイテム(この場合はIPアドレス)をに追加します。 your_list
. CloudflareAPIはあなたを連れて行きます ACCOUNT_MAIL
と API_KEY
リクエストのヘッダーにキーを次のように指定します X-Auth-Email
と X-Auth-Key
. リクエストの本文は、オブジェクトの配列を取ります ip
リストに追加するIPアドレスとして、および comment
値で your_comment
エントリを識別します。 の値を変更できます comment
あなた自身のカスタムコメントで。 経由で行われたPOSTリクエスト axios.post()
発生する可能性のあるエラーがある場合はそれを処理するために、try-catchブロックでラップされます。 The axios.post
関数は url
, body
とオブジェクト headers
リクエストを行います。
変更 clientIP
内の変数 ipMiddleware
次のようなテストIPアドレスを使用してAPIリクエストをテストするときに機能します 198.51.100.0/24
CloudflareはそのリストでローカルホストのIPアドレスを受け入れないため。
...
let clientIP = '198.51.100.0/24';
...
ターミナルウィンドウにアクセスして、アプリケーションを実行します。
- node server.js
次に、 localhost:3000
Webブラウザで。 ブラウザウィンドウに次のように表示されます。 Successful response
. ページを繰り返し更新して、 RPS_LIMIT
. ターミナルウィンドウに次のように表示されます。
OutputExample app is listening on port 3000
You are hitting limit ::1
制限に達したら、Cloudflareダッシュボードを開いて、 your_list
のページ。 Cloudflareのリストに追加されたコードに入力したIPアドレスが表示されます your_list
. 変更をGitHubにプッシュすると、ファイアウォールページが表示されます。
<$>[警告] 警告: 必ず値を変更してください clientIP
に可変 requestIP.getClientIp(req)
コードをGitHubにデプロイまたはプッシュする前。
変更をコミットし、コードをGitHubにプッシュして、アプリケーションをデプロイします。 自動デプロイを設定すると、GitHubのコードがDigitalOceanのAppPlatformに自動的にデプロイされます。 あなたのように .env
ファイルはGitHubに追加されません。ファイルは、アプリレベルの環境変数セクションの設定タブからAppPlatformに追加する必要があります。 プロジェクトのキーと値のペアを追加します .env
アプリケーションがAppPlatform上のコンテンツにアクセスできるようにするためのファイル。 環境変数を保存したら、を開きます your_domain
展開が完了した後、ブラウザでページを繰り返し更新して、 RPS_LIMIT
. 制限に達すると、ブラウザにCloudflareのファイアウォールページが表示されます。
AppPlatformダッシュボードのRuntimeLogs タブに移動すると、次の出力が表示されます。
Output...
You are hitting limit your_public_ip
開くことができます your_domain
別のデバイスから、またはVPN経由で、ファイアウォールがIPアドレスのみを禁止していることを確認します。 your_list
. からIPアドレスを削除できます your_list
Cloudflareダッシュボードを介して。
注:ブラウザからの応答がキャッシュされているため、ファイアウォールがトリガーされるまでに数秒かかる場合があります。
Cloudflare APIを呼び出して、ユーザーがレート制限に達したときにIPアドレスをブロックするようにCloudflareのファイアウォールを設定しました。
結論
この記事では、Cloudflareを介してルーティングされたドメインに接続されたDigitalOceanのAppPlatformにデプロイされたNode.jsプロジェクトを構築しました。 Cloudflareでファイアウォールルールを設定することにより、レート制限の誤用からドメインを保護しました。 ここから、ユーザーを禁止する代わりに、ファイアウォールルールを変更してJSチャレンジまたはCAPTCHAを表示できます。 Cloudflareのドキュメントにプロセスの詳細が記載されています。