AppPlatformでNode.jsを使用してレートリミッターを構築する方法
著者はCOVID-19救済基金を選択し、 Write forDOnationsプログラムの一環として寄付を受け取りました。
序章
レート制限は、ネットワークのトラフィックを管理し、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
ファイルに次の行を追加します。
.gitignore
node_modules/ .env
.gitignore
ファイルの最初の行は、node_modules
ディレクトリを追跡しないようにgitに指示するものです。 これにより、リポジトリのサイズを小さく保つことができます。 node_modules
は、コマンドnpm install
を実行することにより、必要に応じて生成できます。 2行目は、環境変数ファイルが追跡されないようにします。 次の手順で.env
ファイルを作成します。
コードエディタでserver.js
に移動し、次のコード行を変更します。
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 、ルートとして @ を選択し、[に貼り付けますX134X]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(ランダムアクセスメモリ)などのクイックアクセスハードウェアに保持されます。 キャッシュの基本的な目標は、キャッシュの下にある低速のストレージレイヤーにアクセスする必要性を減らすことで、データ取得のパフォーマンスを向上させることです。 プロセスを支援するために、node-cache
、is-ip
、およびrequest-ip
の3つのnpmパッケージを使用します。
request-ip
パッケージは、サーバーの要求に使用されるユーザーのIPアドレスをキャプチャします。 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');
の下に次のコード行を追加します。
server.js
... const requestIP = require('request-ip'); const nodeCache = require('node-cache'); const isIp = require('is-ip'); ...
ここの最初の行は、インストールしたrequest-ip
パッケージからrequestIP
モジュールを取得します。 このモジュールは、サーバーの要求に使用されるユーザーのIPアドレスをキャプチャします。 2行目は、node-cache
パッケージからnodeCache
モジュールを取得します。 nodeCache
はメモリ内キャッシュを作成します。これを使用して、1秒あたりのユーザーのリクエストを追跡します。 3行目は、is-ip
パッケージからisIp
モジュールを取得します。 これは、IPアドレスがIPv6であるかどうかをチェックします。これは、Cloudflareの仕様に従ってCIDR表記を使用するようにフォーマットします。
server.js
ファイルで定数変数のセットを定義します。 これらの定数は、アプリケーション全体で使用します。
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
は、アプリケーションがユーザーのタイムスタンプを平均化する期間を決定する定数変数です。 期間を長くすると、キャッシュサイズが大きくなるため、より多くのメモリを消費します。 TIME_FRAME_IN_MS
定数変数は、アプリケーションがユーザーのタイムスタンプを平均化する期間も決定しますが、ミリ秒単位です。 MS_TO_S
は、ミリ秒単位の時間を秒単位に変換するために使用する変換係数です。 RPS_LIMIT
変数は、レートリミッターをトリガーし、アプリケーションの要件に従って値を変更するアプリケーションのしきい値制限です。 RPS_LIMIT
変数の値2
は、開発フェーズ中にトリガーされる中程度の値です。
Expressを使用すると、サーバーに送信されるすべてのHTTPリクエストにアクセスできるミドルウェア関数を記述して使用できます。 ミドルウェア関数を定義するには、app.use()
を呼び出して関数を渡します。 ミドルウェアとしてipMiddleware
という名前の関数を作成します。
server.js
... 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); ...
requestIP
が提供するgetClientIp()
関数は、ミドルウェアからの要求オブジェクトreq
をパラメーターとして受け取ります。 .v6()
関数はis-ip
モジュールから取得され、渡された引数がIPv6アドレスの場合はtrue
を返します。 Cloudflareのリストには、/64
CIDR表記のIPv6アドレスが必要です。 aaaa:bbbb:cccc:dddd::/64
の形式に従うように、IPv6アドレスをフォーマットする必要があります。 .split(':')メソッドは、IPアドレスを含む文字列から配列を作成し、文字:
で分割します。 .splice(0,4)メソッドは、配列の最初の4つの要素を返します。 .join(':')
メソッドは、文字:
と組み合わせた配列から文字列を返します。
next()
呼び出しは、ミドルウェアが存在する場合、次のミドルウェア関数に移動するようにミドルウェアに指示します。 あなたの例では、GETルート/
へのリクエストを受け取ります。 これは、関数の最後に含めることが重要です。 そうしないと、リクエストはミドルウェアから転送されません。
定数の下に次の変数を追加して、node-cache
のインスタンスを初期化します。
server.js
... const IPCache = new nodeCache({ stdTTL: TIME_FRAME_IN_S, deleteOnExpire: false, checkperiod: TIME_FRAME_IN_S }); ...
定数変数IPCache
を使用すると、nodeCache
にネイティブなデフォルトのパラメーターをカスタムプロパティでオーバーライドします。
stdTTL
:キャッシュ要素のキーと値のペアがキャッシュから削除されるまでの秒単位の間隔。TTL
は、 Time To Live の略で、キャッシュが期限切れになるまでの時間の尺度です。deleteOnExpire
:expired
イベントを処理するカスタムコールバック関数を作成するため、false
に設定します。checkperiod
:期限切れの要素の自動チェックがトリガーされるまでの秒単位の間隔。 デフォルト値は600
であり、アプリケーションの要素の有効期限が小さい値に設定されているため、有効期限のチェックもより早く行われます。
node-cache
のデフォルトパラメータの詳細については、node-cachenpmパッケージのドキュメントページが役立ちます。 次の図は、キャッシュがデータを格納する方法を視覚化するのに役立ちます。
ここで、新しいIPアドレスの新しいキーと値のペアを作成し、IPアドレスがキャッシュに存在する場合は、既存のキーと値のペアに追加します。 値は、アプリケーションに対して行われた各要求に対応するタイムスタンプの配列です。 server.js
ファイルで、IPCache
定数変数の下にupdateCache()
関数を作成して、リクエストのタイムスタンプをキャッシュに追加します。
server.js
... 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()
関数によってキャッチされた現在のタイムスタンプを配列にプッシュしています。 node-cache
によって提供される.set()
関数は、key
、value
、およびTTL
の3つの引数を取ります。 このTTL
は、IPCache
変数からstdTTL
の値を置き換えることにより、標準のTTLセットをオーバーライドします。 IPアドレスがすでにキャッシュに存在する場合は、既存のTTLを使用します。 それ以外の場合は、TTLをTIME_FRAME_IN_S
に設定します。
現在のキーと値のペアのTTLは、有効期限のタイムスタンプから現在のタイムスタンプを差し引くことによって計算されます。 次に、差が秒に変換され、.set()
関数の3番目の引数として渡されます。 。getTtl()
関数は、引数としてキーとIPアドレスを受け取り、キーと値のペアのTTLをタイムスタンプとして返します。 IPアドレスがキャッシュに存在しない場合、undefined
を返し、TIME_FRAME_IN_S
のフォールバック値を使用します。
注: JavaScriptはミリ秒単位で保存するのに対し、node-cache
モジュールは秒を使用するため、ミリ秒から秒への変換タイムスタンプが必要です。
ipMiddleware
ミドルウェアで、if
コードブロックif (isIp.v6(clientIP))
の後に次の行を追加して、アプリケーションを呼び出すIPアドレスの1秒あたりのリクエスト数を計算します。
server.js
... 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); } } ...
最初の行は、宣言したupdateCache()
関数を呼び出して、IPアドレスによって行われた要求のタイムスタンプをキャッシュに追加します。 2行目は、IPアドレスのタイムスタンプの配列を収集します。 タイムスタンプの配列内の要素の数が1より大きく(1秒あたりのリクエストの計算には最低2つのタイムスタンプが必要)、1秒あたりのリクエストが定数で定義したしきい値を超える場合、[ X247X]IPアドレス。 rps
変数は、リクエスト数を時間間隔の差で割って1秒あたりのリクエスト数を計算し、単位を秒に変換します。
プロパティdeleteOnExpire
をIPCache
変数の値false
にデフォルト設定したため、expired
イベントを手動で処理する必要があります。 node-cache
は、expired
イベントでトリガーするコールバック関数を提供します。 IPCache
定数変数の下に次のコード行を追加します。
server.js
... 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)
の後に、コールバック関数に次のコードを追加します。
server.js
... 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でキャッシュが更新されます。 updatedValue
変数の最初の要素に一致するTTLは、キャッシュが次の要素を削除すると、.on('expired')
コールバック関数をトリガーします。 TIME_FRAME_IN_S
と、updatedValue
の最初のリクエストのタイムスタンプからの有効期限の差により、新しく更新されたTTLが計算されます。
ミドルウェア機能が定義されたら、ターミナルウィンドウにアクセスして、アプリケーションを実行します。
node server.js
次に、Webブラウザでlocalhost:3000
にアクセスします。 ブラウザウィンドウに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
を選択し、値にyour_list
を選択します。 。 アクションの選択のドロップダウンで、ブロックを選択し、展開をクリックします。
プロジェクトのルートディレクトリに.env
ファイルを作成し、次の行を使用して、アプリケーションからCloudflareAPIを呼び出します。
.env
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
リストの横にある編集をクリックします。 ブラウザのyour_list
のURLからACCOUNT_ID
とLIST_ID
を取得します。 URLは次の形式です:https://dash.cloudflare.com/your_account_id/configurations/lists/your_list_id
警告: .env
の内容は機密扱いであり、公開されていないことを確認してください。 手順1で作成した.gitignore
ファイルに.env
ファイルがリストされていることを確認してください。 <$>
axiosおよびdotenv
パッケージをnpm経由で端末にインストールします。
npm i axios dotenv
コードエディタでserver.js
ファイルを開き、nodeCache
定数変数の下に次のコード行を追加します。
server.js
... const axios = require('axios'); require('dotenv').config(); ...
ここの最初の行は、インストールしたaxios
パッケージからaxios
モジュールを取得します。 このモジュールを使用して、CloudflareのAPIへのネットワーク呼び出しを行います。 2行目は、dotenv
モジュールを必要とし、.env
ファイルに配置した値をserver.js
に定義するprocess.env
グローバル変数を有効にするように構成します。
console.log('You are hitting limit', clientIP)
の上のipMiddleware
内のif (rps > RPS_LIMIT)
条件に以下を追加して、CloudflareAPIを呼び出します。
server.js
... 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
に追加します。 Cloudflare APIは、X-Auth-Email
およびX-Auth-Key
としてキーを使用して、リクエストのヘッダーにあるACCOUNT_MAIL
およびAPI_KEY
を取得します。 リクエストの本文は、リストに追加するIPアドレスとしてip
を使用し、エントリを識別するために値your_comment
を使用するcomment
を持つオブジェクトの配列を取ります。 comment
の値は、独自のカスタムコメントで変更できます。 axios.post()
を介して行われたPOST要求は、発生する可能性のあるエラーがある場合はそれを処理するために、try-catchブロックでラップされます。 axios.post
関数は、url
、body
、およびheaders
を持つオブジェクトを取得して要求を行います。
Cloudflareはリスト内のローカルホストのIPアドレスを受け入れないため、198.51.100.0/24
などのテストIPアドレスを使用してAPIリクエストをテストする場合は、ipMiddleware
関数内のclientIP
変数を変更してください。
server.js
... let clientIP = '198.51.100.0/24'; ...
ターミナルウィンドウにアクセスして、アプリケーションを実行します。
node server.js
次に、Webブラウザでlocalhost:3000
にアクセスします。 ブラウザウィンドウにSuccessful response
と表示されます。 ページを繰り返し更新して、RPS_LIMIT
を押します。 ターミナルウィンドウに次のように表示されます。
OutputExample app is listening on port 3000 You are hitting limit ::1
制限に達したら、Cloudflareダッシュボードを開き、your_list
'のページに移動します。 your_list
という名前のCloudflareのリストに追加されたコードに入力したIPアドレスが表示されます。 変更を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
別のデバイスから、またはVPN経由でyour_domain
を開くと、ファイアウォールがyour_list
のIPアドレスのみを禁止していることを確認できます。 Cloudflareダッシュボードを介してyour_list
からIPアドレスを削除できます。
注:ブラウザからの応答がキャッシュされているため、ファイアウォールがトリガーされるまでに数秒かかる場合があります。
Cloudflare APIを呼び出して、ユーザーがレート制限に達したときにIPアドレスをブロックするようにCloudflareのファイアウォールを設定しました。
結論
この記事では、Cloudflareを介してルーティングされたドメインに接続されたDigitalOceanのAppPlatformにデプロイされたNode.jsプロジェクトを構築しました。 Cloudflareでファイアウォールルールを設定することにより、レート制限の誤用からドメインを保護しました。 ここから、ユーザーを禁止する代わりに、ファイアウォールルールを変更してJSチャレンジまたはCAPTCHAを表示できます。 Cloudflareのドキュメントにプロセスの詳細が記載されています。