Skip to content

画面操作へのレート制限の追加 ​

第1版作成 最終更新 (日本時間)
確認バージョン1.5.8.1

本体の標準機能ではありません

このページは本体を改修する場合の設計メモです。現行の仕組みはレート制限(API の日次上限と RateLimit.json)を参照してください。

前提にした現行実装(1.5.8.1)は次のとおりです。

  • API とサーバースクリプトのレコード操作は、サイト単位・日次の SiteModel.WithinApiLimits() で数える。画面からの作成・更新・削除は数えない。
  • RateLimit.json によるリクエスト単位の制限(ASP.NET Core のレートリミッター)があり、画面のレコード操作には General ポリシーが掛かる。ただし有効にするにはライセンスの RateLimit オプションが要る。

そのため、改修を考える場面は次の 2 つです。

  1. 画面からの操作も、API と同じ「サイトごとの 1 日の件数」に含めたい(WithinApiLimits の流用)。
  2. RateLimit オプションが無い環境で、リクエスト単位の制限を自前で入れたい(ミドルウェアの自前組み込み)。

RateLimit オプションがあるなら、2 は RateLimit.json の設定で足ります(1.5.8.1 の実装が、ここで検討する方式とほぼ同じ構成です)。

案 1:WithinApiLimits を画面操作にも掛ける ​

現状の分岐 ​

画面の操作は Controllers/ItemsController から ItemModel.Create() などを呼び、API は Controllers/Api/ItemsController から ItemModel.CreateByApi() を呼びます。上限のチェックがあるのは後者だけです。

図を読み込み中…

流用できるもの ​

  • SiteModel.WithinApiLimits(context)(SiteModel.cs#L10081-L10112)。ItemModel は SetSite() の後なら Site で参照できる。
  • Error.Types.OverLimitApi.MessageJson(context, data) で、画面向けの ResponseCollection 形式のエラーを 1 行で返せる。既存の画面処理が Error.Types.ItemsLimit.MessageJson(context) を返しているのと同じ形。
  • Messages.ResponseOverLimitApi() も定義済み(Messages.cs#L4501)だが、1.5.8.1 では呼ばれていない。
  • メッセージ OverLimitApi は多言語化済み(日本語は「ID: {0} のサイトで利用できるAPIの制限({1} 件/日)を超えました。」)。

改修イメージ ​

ItemModel.Create()・Update()・Delete()・BulkDelete() の SetSite() 直後にチェックを入れます。

csharp
public string Create(Context context)
{
    SetSite(
        context: context,
        initSiteSettings: true);
    if (!Site.WithinApiLimits(context: context))
    {
        return Error.Types.OverLimitApi.MessageJson(
            context: context,
            data: new[]
            {
                Site.SiteId.ToString(),
                context.ContractSettings.ApiLimit().ToString()
            });
    }
    switch (Site.ReferenceType)
    {
        // 既存の処理
    }
}

超過すると、画面にはエラーメッセージ(alert-error)が出ます。

注意点 ​

観点内容
カウンターの共有Sites.ApiCount は API と共通。画面と API の両方で使うサイトは上限に早く達する。画面だけ別に数えたいなら Sites に別の列(例: FormCount・FormCountDate)と判定メソッドを足す必要があり、列の追加は CodeDefiner の定義変更になる
並行処理WithinApiLimits() はロックなしの読み取り → 加算 → 書き込みなので、同時操作で上限をわずかに超える
有効化LimitPerSite の既定は 0(無制限)。改修しても Api.json の LimitPerSite かテナントの ApiLimitPerSite を設定しない限り効かない
メッセージ文言が「API の制限」になる。画面用の文言にしたいなら App_Data/Displays/ に表示文字列を足す
1 日単位日次の件数制限なので、短時間の連打(数秒で数百件)を止める用途には向かない。その用途は案 2

案 2:ASP.NET Core のレートリミッターを自前で組み込む ​

.NET 7 以降は Microsoft.AspNetCore.RateLimiting と System.Threading.RateLimiting がフレームワークに含まれているので、NuGet の追加は要りません(1.5.8.1 は net10.0)。

WithinApiLimits との違い ​

項目WithinApiLimitsレートリミッター
単位サイト任意(ユーザー・IP・API キー・テナントなど)
時間枠1 日固定秒〜日で自由
アルゴリズム固定の日次カウントFixedWindow・SlidingWindow・TokenBucket・Concurrency
カウンターDB(Sites)プロセスのメモリ
並行処理ロックなしフレームワーク内で排他済み
判定の場所モデルの中HTTP ミドルウェア(コントローラーの手前)
複数台構成DB 共有なので台数に関係なく共通サーバーごとに別。実効上限は設定値 × 台数

構成 ​

1.5.8.1 の RateLimit.json と同じく、次の 3 点で組み込みます。

  1. Startup.ConfigureServices で services.AddRateLimiter() を呼び、名前付きポリシーを登録する。
  2. Startup.Configure の UseAuthentication()・UseAuthorization() の後に app.UseRateLimiter() を置く(認証後に置くとユーザー単位のキーが使える。1.5.8.1 もこの位置。Startup.cs#L653-L664)。
  3. コントローラーかアクションに [EnableRateLimiting("ポリシー名")] を付ける。除外するアクションは [DisableRateLimiting]。ルート単位なら MapControllerRoute(...).RequireRateLimiting("ポリシー名")。
csharp
services.AddRateLimiter(options =>
{
    options.AddPolicy("FormPost", httpContext =>
    {
        var key = httpContext.User?.Identity?.IsAuthenticated == true
            ? httpContext.User.Identity.Name
            : httpContext.Connection.RemoteIpAddress?.ToString() ?? "unknown";
        return RateLimitPartition.GetFixedWindowLimiter(
            partitionKey: key,
            factory: _ => new FixedWindowRateLimiterOptions
            {
                PermitLimit = 100,
                Window = TimeSpan.FromMinutes(1),
                QueueLimit = 0
            });
    });
    options.RejectionStatusCode = StatusCodes.Status429TooManyRequests;
});

キーの候補は次のとおりです。

キー取得元特徴
IP アドレスHttpContext.Connection.RemoteIpAddress未認証のリクエストにも使える。NAT やプロキシの後ろでは共有される
ユーザーHttpContext.User.Identity.Name(ログイン ID)認証済みのみ
テナントContext.TenantIdマルチテナント向き
サイトルートの id から解決WithinApiLimits と同じ粒度だが、ルートからの抽出が必要

上限値はハードコードせず、App_Data/Parameters/ に JSON を置いて Implem.ParameterAccessor/Parts にクラスを作り、Initializer.SetParameters() で読み込むのがプリザンターの流儀です。1.5.8.1 の RateLimit.json と Parts/RateLimit.cs がその実例なので、項目の設計はそれに合わせると移行しやすくなります。

拒否時の応答 ​

画面の操作は Ajax で、応答は ResponseCollection 形式の JSON です。ミドルウェアが素の 429 を返すと、画面側は通常のエラー処理に落ちてメッセージが出ません。OnRejected で、X-Requested-With: XMLHttpRequest のリクエストには ResponseCollection 形式を返すと、既存のメッセージ表示に乗ります。1.5.8.1 の BodyRateLimitHelper.OnRejectedAsync() はこの切り替えをしています(BodyRateLimitHelper.cs#L311-L364)。

csharp
options.OnRejected = async (ctx, cancellationToken) =>
{
    var response = ctx.HttpContext.Response;
    response.ContentType = "application/json;charset=utf-8";
    if (ctx.HttpContext.Request.Headers["X-Requested-With"] == "XMLHttpRequest")
    {
        var rc = new ResponseCollection();
        rc.Message(
            message: new Message(id: null, text: "リクエストが多すぎます。", css: "alert-error"),
            target: "#Message");
        await response.WriteAsync(rc.ToJson(), cancellationToken);
    }
    else
    {
        await response.WriteAsJsonAsync(
            new { StatusCode = 429, Message = "Too many requests." },
            cancellationToken);
    }
};

複数台構成 ​

カウンターはプロセスのメモリにあるので、ロードバランサーの後ろに複数台並べるとサーバーごとに別々に数えます。台数に関係なく共通の上限にしたい場合は、PartitionedRateLimiter を自作して Redis などにカウンターを置きます。プリザンターは StackExchange.Redis を参照済みです(Implem.Pleasanter.csproj#L84)。

WithinApiLimits との併用 ​

API にミドルウェアの制限を掛けると、API は「ミドルウェアの短期の制限」と「WithinApiLimits の日次の件数」の二重になります。両者は目的が違う(瞬間的な集中と 1 日の総量)ので併用して構いません。ミドルウェアを画面だけに掛けるなら、API コントローラーには付けないか [DisableRateLimiting] を付けます。

関連ページ ​

変更履歴

第1版レートリミッターの解説と、フォーム投稿の制限・ウイルススキャン・拡張子制限・外部検索エンジン・管理画面設定の改修・設計メモを追加