【Express.js】リクエスト制限(レートリミット)設定

この記事では、express-rate-limitミドルウェアを利用してリクエスト数を制限する方法について解説する。

express-rate-limitとは

express-rate-limitは、同一IPアドレスからのリクエストを制御する仕組みを提供する。これにより、特定のIPアドレスからのリクエスト数を監視し、制限を超えた場合にエラーレスポンスを返す。一般的に、悪意あるアクセスは同一IPから発生することが多いため、この制御はスパムやDoS攻撃の防止に有効となる。

express-rate-limitのGitHubリポジトリ
https://github.com/express-rate-limit/express-rate-limit

実装のシチュエーション

今回の実装では、検索履歴を保存するエンドポイントに対して、リクエスト数を制限する設定を行う。

具体的には、以下の要件を満たすように実装する。

  • 目的:検索履歴を保存するAPIに対して、連続リクエストによるスパム行為を防ぐ。
  • 設定:1分間に10回のリクエストが可能となるように設定し、それ以上のリクエストにはエラーレスポンスを返す。

実装

実装手順は以下の通り。

  1. リクエスト数を制限するミドルウェアを作成する
  2. 対象のエンドポイントでミドルウェアを実行する
  3. 結果を検証する

1.ミドルウェアの実装

src/middleware/rateLimiter.tsを作成し、以下の実装を行う。

import rateLimit from "express-rate-limit";

/**
 * レートリミットの設定
 * サーバー側でもリクエストの頻度を制限することで、DoS攻撃やスパム行為を防止する
 */
export const searchRateLimiter = rateLimit({
  windowMs: 60 * 1000, // 1分間のウィンドウ
  max: 10, // 1分間に最大10回のリクエストを許可
  message: "Too many requests, please try again later.",
  standardHeaders: true, // Rate limit info を `RateLimit-*` headers に表示
  legacyHeaders: false, // `X-RateLimit-*` headers を無効化
});
  • windowMs:リクエスト数を監視する時間(ミリ秒単位)
  • limit:指定時間内(windowMs)で許可するリクエスト数
  • message:制限に達した場合に返されるメッセージ
  • standardHeadersRateLimit-*ヘッダーを使用するかどうか
  • legacyHeadersX-RateLimit-*ヘッダーの有効化(デフォルトはfalse)

standardHeadersオプションについて

このオプションをtrueに設定すると、RateLimitヘッダーが使用される。
RateLimitヘッダーは、以下の情報を提供する。

  • RateLimit-Limit:指定された時間枠内で許可されているリクエストの最大数
  • RateLimit-Remaining:残りのリクエスト数
  • RateLimit-Reset:リクエスト制限がリセットされるまでの時間(秒)

これにより、クライアントは現在のリクエスト制限のステータスを把握できる。フロントエンドでリクエスト制限の状態を表示する場合などに使用できる。

2.ミドルウェアの実行

上記のミドルウェア(searchRateLimiter)を検索履歴の保存APIに適用する。

import express from "express";
import {
  createSearchHistory,
} from "../controllers/search/SearchHistoryController";
import { searchRateLimiter } from "../middleware/rateLimiter";

const router = express.Router();

/**
 * キーワード検索履歴を保存するエンドポイント
 */
router.post("/search-history/create", searchRateLimiter, createSearchHistory);

export default router;

3.結果の検証

上記の設定により、1分間に10回のリクエストが許可され、それ以上のリクエストには429 Too Many Requestsが返される。エラーレスポンスには、指定したメッセージ「Too many requests, please try again later.」が含まれる。

express-rate-limitの追加解説

オプション設定

上記で使用したオプション以外にも、使えそうなものがいくつかあるので解説する。

オプション説明
statusCodenumber上限に達したときのHTTPステータスコードを指定する(デフォルトは429)。
handlerfunction上限に達した際に実行される関数を指定する。
【ユースケース】
messageやstatusCodeの設定を上書きするときに使用する。
storeStoreカスタムストアを利用して、複数のノード間でヒットカウントを共有する。
keyGeneratorfunctionユーザーの識別に使用する情報を指定する(デフォルトはIPアドレス)。
skipfunction指定したリクエストをリミットの対象から除外するかどうかを決定する関数。trueを返すとリミッターがバイパスされる。

store、keyGenerator、skipについて、具体的なユースケースを解説する。

store

storeは、負荷分散環境下にて「リクエスト数の一元管理」を行うために使用する。

例えば、Webアプリケーションがアクセス集中を防ぐために複数のサーバーで運用されているとする。

この構成では、ユーザーがサイトにアクセスするたびに、異なるサーバー(サーバーA、サーバーB、サーバーCなど)にランダムに接続される仕組みになっている。これを負荷分散と呼ぶ。

例えば、リクエスト上限を「15分間に100リクエスト」と設定しているとする。

ユーザーがサーバーAに対して50回のリクエストを行い、
次にサーバーBに対してさらに50回リクエストを行ったとする。

この場合、各サーバーが独自のリクエストカウントを保持していると、
サーバーAでのカウントは「50」、
サーバーBでのカウントも「50」となり、
合計100回を超えたのに制限がかからない場合がある。

こうした状況を回避するために、storeオプションを使ってRedisMemcachedといった外部ストアを利用し、リクエスト数を一元的に管理する。

実装例

import rateLimit from 'express-rate-limit';
import RedisStore from 'rate-limit-redis';
import redis from 'redis';

const redisClient = redis.createClient({
  host: 'localhost',
  port: 6379,
});

const limiter = rateLimit({
  store: new RedisStore({
    client: redisClient,
  }),
  windowMs: 15 * 60 * 1000, // 15分間
  max: 100, // 各IPが15分間に最大100回リクエスト可能
});

app.use(limiter);

処理イメージ

  1. ユーザーからリクエストが送信されると、そのリクエストはまずロードバランサーを通じて、複数のサーバーのうちどれか1台に振り分けられる。
  2. サーバーがRedisに接続し、そのユーザーのリクエストカウントを管理する。Redisは全サーバー間で共有されているため、どのサーバーにリクエストが振り分けられても、リクエスト数は統一して管理される。
  3. サーバーは、ユーザーを一意に判定するためにIPアドレスやユーザーIDなどをキーとして利用し、そのキーをもとにRedis内でリクエスト数をカウントする。

これにより、同じユーザーが複数回リクエストを送信しても、サーバー間で一貫したリクエスト制限が適用される。

keyGenerator

同じIPアドレスから異なるユーザーがアクセスする状況(例:企業内のネットワーク)で、IPアドレスに基づくリミットだと制限がかかりやすくなる。そこで、APIキーユーザーIDなど、リクエストヘッダーや認証トークンに含まれる情報を使ってユーザーを識別するために使用する。企業向けのシステムなどでレートリミット設定をする場合は、使用することがある。

実装例

const limiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15分間
  max: 100,
  keyGenerator: (req) => {
    // リクエストヘッダーからAPIキーを取得
    return req.headers['api-key'];
  },
});

app.use(limiter);

skip

特定の条件に該当するリクエストにはレートリミットを適用したくない場合がある。たとえば、認証済みの管理者ユーザーや、サイトの開発者など、特定のIPアドレスからのリクエストは無制限に許可したいといったケースで使用する。

実装例

const limiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15分間
  max: 100,
  skip: (req) => {
    // 管理者ユーザー(req.user.roleが"admin")の場合、レートリミットをスキップ
    return req.user && req.user.role === 'admin';
  },
});

app.use(limiter);

リクエスト制限(レートリミット)を行う目的

  • DoS(Denial of Service)攻撃の防止
  • スパム行為の防止
  • サーバーリソースの保護

DoS(Denial of Service)攻撃の防止

DoS攻撃とは、悪意のあるユーザーが大量のリクエストを送り込むことでサーバーに過負荷をかけ、サービスをダウンさせようとする攻撃である。サーバーがダウンすると、他の正当なユーザーがサービスを利用できなくなる恐れがあり、復旧にも時間を要することがある。レートリミットを設定することで、同一IPアドレスからの過剰なリクエストをブロックし、サーバーに過負荷がかかるのを防ぐことができる。

スパム行為の防止

一部のユーザーやボットがAPIを悪用し、大量のリクエストを送信することで、データを不正に収集したり、不要なデータを書き込んだりする場合がある。これらのスパム行為は、データベースの容量を圧迫したり、他のユーザーの体験を損なう原因となる。特に、認証・認可を必要としないサービスでユーザーの入力が発生する場合は注意が必要。リクエスト制限を設けることで、特定のユーザーやボットが短時間に繰り返しアクセスするのを防ぎ、APIの健全性を確保する。

サーバーリソースの保護

サーバーリソース(CPU、メモリ、帯域幅など)は限られており、過剰なリクエストによってこれらのリソースが消費されると、他のユーザーに悪影響を及ぼす可能性がある。リソースの枯渇により、正当なユーザーがリクエストに対して遅延を感じたり、最悪の場合にはサーバーが応答できなくなることもある。レートリミットを設定することで、サーバーが一度に処理するリクエスト数を制限し、リソースを適切に管理できる。

まとめ

express-rate-limitを利用することで、APIに対するリクエスト数の制御が容易になり、スパムやDoS攻撃のリスクを軽減できる。今回は、検索履歴を保存するAPIでのユースケースを例に、1分間に10回のリクエストまで許可する設定を紹介した。あなたの開発の参考になれば幸いだ。

関連記事

コメント

この記事へのトラックバックはありません。