lru-cacheを使ってIPアドレスでAPIにレートリミットを追加する
はじめに
先日、AnalyzeMeというアプリをリリースしました。
このアプリをリリースするにあたり、APIを叩かれ過ぎないようにレートリミットを追加したので今回はそのことについて書きます。
catnoseさんの未ログインでも叩けるAPIエンドポイントにレートリミットを導入するを参考に実装しようとも思ったのですが、Redisを利用していなかったのと、今回のアプリの場合そもそも利用者が限られるアプリのため別の実装方法を用いることにしました。今回はNext.jsが出しているサンプルリポジトリlru-cacheに基づいてレートリミットを追加することにします。
上述のNext.jsのサンプルリポジトリとはapi-routes-rate-limitのことです。
更新が行われていないこともあり、多少修正を加える必要がありましたが、ほとんどこのリポジトリの通りで問題ありませんでした。非常にありがたいです。
実装方法
まずはlru-cacheをインストールします。
yarn add lru-cache
src/utils/rateLimit.ts
を作成して、以下のコードを書きます。
import { LRUCache } from "lru-cache";
export const rateLimit = () => {
const tokenCache = new LRUCache<string, number>({
max: 500, // 各intervalごとで何人のユーザーを許容するか
ttl: 1000 * 60 * 5, // intervalの時間(ミリ秒)
});
return {
check: (limit: number, token: string): Promise<void> =>
new Promise((resolve, reject) => {
const tokenCount = tokenCache.get(token) || 0;
const currentUsage = tokenCount + 1;
tokenCache.set(token, currentUsage);
const isRateLimited = currentUsage > limit;
const headersList = new Headers();
headersList.set("X-RateLimit-Limit", String(limit));
headersList.set(
"X-RateLimit-Remaining",
isRateLimited ? "0" : String(limit - currentUsage)
);
return isRateLimited ? reject() : resolve();
}),
};
};
これは特定のIPアドレス(token)を用いているユーザーがインターバル(ttl)の間に最大何回APIを叩けるか(limit)で制限をかけており、制限を超えるとrejectされます。
呼び出す時は以下のような感じで使えます。
// src/app/api/user/route.ts
import { rateLimit } from "@/utils/rateLimit";
const limiter = rateLimit();
export async function POST(req: NextRequest): Promise<Response> {
let ip = req.ip ?? req.headers.get("x-real-ip") ?? "";
const forwardedFor = req.headers.get("x-forwarded-for");
if (!ip && forwardedFor) {
ip = forwardedFor.split(",").at(0) ?? "Unknown";
}
try {
await limiter.check(5, ip); // 同一IPアドレスからのアクセスはインターバルで最大5回まで
} catch (error) {
return new Response("Rate Limited", { status: 429 });
}
// 以下略
もし匿名ログイン等を使っている場合、各ユーザーにトークンが発行されているようであれば、IPアドレスの代わりにtokenを入れればより細かい制御が可能になると思います。
最後に
というわけで簡単にIPアドレスでの制御ができました。もちろんログインをしていないので、制御できる範囲には限りがありますが、少なくとも悪意を持ったDDoS攻撃などは避けることができるのではないでしょうか。
Next.jsのサンプリリポジトリは趣味で結構読んでいるんですが、すぐには使えなくても頭の片隅にでも置いておくとふとしたときにこういう実装方法あった気がするということで思い出して利用できて良いですね。
この記事が少しでも誰かの参考になれば幸いです。
Discussion