序章

レート制限は、ネットワークのトラフィックを管理し、APIの使用など、特定の期間に誰かが操作を繰り返す回数を制限します。 レート制限の乱用に対するセキュリティの層がないサービスは、過負荷になりがちであり、正当な顧客に対するアプリケーションの適切な動作を妨げます。

このチュートリアルでは、リクエストのIPアドレスを確認し、ユーザーごとのリクエストのタイムスタンプを比較してこれらのリクエストの割合を計算するNode.jsサーバーを構築します。 IPアドレスがアプリケーションに設定した制限を超える場合は、CloudflareのAPIを呼び出し、IPアドレスをリストに追加します。 次に、リストにIPアドレスを持つすべてのリクエストを禁止するCloudflareファイアウォールルールを構成します。

このチュートリアルの終わりまでに、DigitalOceanの App Platform にデプロイされたNode.jsプロジェクトを構築し、レート制限でCloudflareルーティングドメインを保護します。

前提条件

このガイドを開始する前に、次のものが必要です。

ステップ1— Node.jsプロジェクトをセットアップし、DigitalOceanのアプリプラットフォームにデプロイする

このステップでは、基本的なExpressサーバーを拡張し、コードをGitHubリポジトリにプッシュして、アプリケーションをAppPlatformにデプロイします。

コードエディタを使用して、基本的なExpressサーバーのプロジェクトディレクトリを開きます。 名前で新しいファイルを作成します .gitignore プロジェクトのルートディレクトリにあります。 新しく作成した行に次の行を追加します .gitignore ファイル:

.gitignore
node_modules/
.env

あなたの最初の行 .gitignore fileは、gitが追跡しないようにするためのディレクティブです。 node_modules ディレクトリ。 これにより、リポジトリのサイズを小さく保つことができます。 The 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( `)で囲まれています。 これにより、テンプレートリテラルを使用できるようになります。これにより、文字列内に式を含めることができます。

ターミナルウィンドウにアクセスして、アプリケーションを実行します。

  1. node server.js

ブラウザウィンドウが表示されます Successful response. ターミナルに、次の出力が表示されます。

Output
Example 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 タブに移動すると、次の出力が表示されます。

Output
Example 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経由でパッケージ化します。

  1. 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');
...

ここの最初の行は requestIP からのモジュール request-ip インストールしたパッケージ。 このモジュールは、サーバーの要求に使用されるユーザーのIPアドレスをキャプチャします。 2行目は nodeCache からのモジュール node-cache パッケージ。 nodeCache 1秒あたりのユーザーのリクエストを追跡するために使用するメモリ内キャッシュを作成します。 3行目は isIp からのモジュール is-ip パッケージ。 これは、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 アプリケーションがユーザーのタイムスタンプを平均化する期間を決定する定数変数です。 期間を長くすると、キャッシュサイズが大きくなるため、より多くのメモリを消費します。 The TIME_FRAME_IN_MS 定数変数は、アプリケーションがユーザーのタイムスタンプを平均化する期間も決定しますが、ミリ秒単位です。 MS_TO_S ミリ秒単位の時間を秒に変換するために使用する変換係数です。 The RPS_LIMIT variableは、レートリミッターをトリガーし、アプリケーションの要件に従って値を変更するアプリケーションのしきい値制限です。 値 2 の中に RPS_LIMIT 変数は、開発フェーズ中にトリガーされる中程度の値です。

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);

...

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 定数の下に次の変数を追加します。

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: に設定 false カスタムコールバック関数を記述して、 expired イベント。
  • checkperiod:期限切れの要素の自動チェックがトリガーされるまでの秒単位の間隔。 デフォルト値は 600、およびアプリケーションの要素の有効期限が小さい値に設定されているため、有効期限のチェックもより早く行われます。

のデフォルトパラメータの詳細については node-cachenode-cachenpmパッケージのドキュメントページが便利です。 次の図は、キャッシュがデータを格納する方法を視覚化するのに役立ちます。

ここで、新しいIPアドレスの新しいキーと値のペアを作成し、IPアドレスがキャッシュに存在する場合は、既存のキーと値のペアに追加します。 値は、アプリケーションに対して行われた各要求に対応するタイムスタンプの配列です。 あなたの中で server.js ファイル、作成 updateCache() 下の機能 IPCache キャッシュへのリクエストのタイムスタンプを追加する定数変数:

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() 配列に機能します。 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秒あたりのリクエスト数を計算するには:

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);
        }
    }
...

最初の行は、IPアドレスによって行われた要求のタイムスタンプを呼び出してキャッシュに追加します。 updateCache() 宣言した関数。 2行目は、IPアドレスのタイムスタンプの配列を収集します。 タイムスタンプの配列内の要素の数が1より大きく(1秒あたりのリクエストの計算には最低2つのタイムスタンプが必要)、1秒あたりのリクエストが定数で定義したしきい値を超える場合は、次のようになります。 console.log IPアドレス。 The rps 変数は、リクエスト数を時間間隔の差で割って1秒あたりのリクエスト数を計算し、単位を秒に変換します。

プロパティをデフォルトにしたので deleteOnExpire 値に false の中に IPCache 変数の場合、処理する必要があります 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() を受け入れるコールバック関数です keyvalue 引数として期限切れの要素の。 キャッシュ内で、 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。 の最初の要素に一致するTTL updatedValue 変数はトリガーします .on('expired') キャッシュが次の要素を削除したときのコールバック関数。 の違い TIME_FRAME_IN_S の最初のリクエストのタイムスタンプからの有効期限 updatedValue 新規および更新されたTTLを計算します。

ミドルウェア機能が定義されたら、ターミナルウィンドウにアクセスして、アプリケーションを実行します。

  1. node server.js

次に、 localhost:3000 Webブラウザで。 ブラウザウィンドウに次のように表示されます。 Successful response. ページを繰り返し更新して、 RPS_LIMIT. ターミナルウィンドウに次のように表示されます。

Output
Example 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を呼び出すために、次の行を含むプロジェクトのルートディレクトリにファイルします。

.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 作成したリスト。 取得する ACCOUNT_IDLIST_ID のURLから your_list ブラウザで。 URLは次の形式です。 https://dash.cloudflare.com/your_account_id/configurations/lists/your_list_id

警告:の内容を確認してください .env 機密が保持され、公開されません。 あなたが持っていることを確認してください .env にリストされているファイル .gitignore 手順1で作成したファイル。 <$>

axiosをインストールして dotenv ターミナルのnpm経由でパッケージ化します。

  1. npm i axios dotenv

を開きます server.js コードエディタでファイルを作成し、その下に次のコード行を追加します。 nodeCache 定数変数:

server.js
...
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を呼び出します。

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. CloudflareAPIはあなたを連れて行きます ACCOUNT_MAILAPI_KEY リクエストのヘッダーにキーを次のように指定します X-Auth-EmailX-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アドレスを受け入れないため。

server.js
...
let clientIP = '198.51.100.0/24';
...

ターミナルウィンドウにアクセスして、アプリケーションを実行します。

  1. node server.js

次に、 localhost:3000 Webブラウザで。 ブラウザウィンドウに次のように表示されます。 Successful response. ページを繰り返し更新して、 RPS_LIMIT. ターミナルウィンドウに次のように表示されます。

Output
Example 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のドキュメントにプロセスの詳細が記載されています。