api-shield
nuxt-api-shield

Nuxt API Shield - 速率限制

Nuxt API Shield

npm versionnpm downloadsLicenseNuxt

這個 Nuxt 模組實現了一個速率限制中介軟體,用於保護您的 API 端點免受過多請求的影響。

功能

  • 基於 IP 的速率限制和暴力破解保護
    • 跟蹤並強制執行單個 IP 地址的速率限制。
    • 防止惡意行為者或來自單一來源的過多請求使您的 API 不堪重負。
  • 可定製的速率限制
    • 配置最大請求計數、限制適用的持續時間,以及超過限制後的停用期。
    • 如果超過請求限制,使用者將被停用,停用期為配置的停用期。在停用期間,所有請求都將收到 429 錯誤並被阻止,無論速率限制視窗如何。
    • 當用戶被停用時,新增響應延遲以阻止進一步濫用(可選)。
    • 自定義被停用使用者的錯誤訊息。
    • 當用戶被停用時,可選地在響應中包含 Retry-After 頭。
    • 根據您的 API 的具體需求和使用模式調整速率限制行為。
  • 事件驅動處理
    • 利用 Nuxt 的事件系統高效攔截傳入的 API 請求。
    • 確保與您的 Nuxt 應用程式的請求生命週期無縫整合。
  • 靈活儲存
    • 利用 Nuxt 的 unstorage 抽象來實現多功能的儲存選項。
    • 根據您的專案要求,將速率限制資料儲存在各種儲存提供商(檔案系統、記憶體、資料庫等)中。
  • 可使用執行時配置進行配置
    • 無需程式碼更改即可輕鬆調整速率限制引數。
    • 透過 Nuxt 的執行時配置適應動態需求並保持對速率限制行為的控制。
  • 清晰的錯誤處理
    • 當超過速率限制或使用者被停用時,返回標準化的 429 “Too Many Requests” 錯誤響應。
    • 促進客戶端應用程式中正確的錯誤處理,以實現流暢的使用者體驗。

快速設定

1. 將 nuxt-api-shield 依賴項新增到您的專案

# Using pnpm
pnpm add nuxt-api-shield

# Using yarn
yarn add nuxt-api-shield

# Using npm
npm install nuxt-api-shield

2. 將 nuxt-api-shield 新增到 nuxt.config.tsmodules 部分

您應該只新增與預設值不同的值。

export default defineNuxtConfig({
  modules: ["nuxt-api-shield"],
  nuxtApiShield: {
    /*limit: {
      max: 12,        // maximum requests per duration time, default is 12/duration
      duration: 108,   // duration time in seconds, default is 108 seconds
      ban: 3600,      // ban time in seconds, default is 3600 seconds = 1 hour
      // If the request limit is exceeded, the user is banned for this period. During the ban, all requests are blocked with 429.
    },
    delayOnBan: true  // delay every response with +1sec when the user is banned, default is true
    errorMessage: "Too Many Requests",  // error message when the user is banned, default is "Too Many Requests"
    retryAfterHeader: false, // when the user is banned add the Retry-After header to the response, default is false
    log: {
      path: "logs", // path to the log file, every day a new log file will be created, use "" to disable logging
      attempts: 100,    // if an IP reach 100 requests, all the requests will be logged, can be used for further analysis or blocking for example with fail2ban, use 0 to disable logging
    },
    routes: [], // specify routes to apply rate limiting to, default is an empty array meaning all routes are protected.
    // Example:
    // routes: ["/api/v2/", "/api/v3/"], // /api/v1 will not be protected, /api/v2/ and /api/v3/ will be protected */
    ipTTL: 604800, // Optional: Time-to-live in seconds for IP tracking entries (default: 7 days). Set to 0 or negative to disable this specific cleanup.
    security: { // Optional: Security-related configurations
      trustXForwardedFor: true, // Default: true. Whether to trust X-Forwarded-For headers. See warning below.
    }
  },
});

預設配置值:(如果您的 nuxtApiShield 配置中未指定,則模組將應用這些值)

{
  limit: {
    max: 12,
    duration: 108, // seconds
    ban: 3600,     // seconds
  },
  delayOnBan: true,
  errorMessage: "Too Many Requests",
  retryAfterHeader: false,
  log: {
    path: "logs", // Logging is disabled if path is empty
    attempts: 100, // Logging per IP is disabled if attempts is 0
  },
  routes: [],
  ipTTL: 7 * 24 * 60 * 60, // 7 days in seconds
  security: {
    trustXForwardedFor: true,
  }
}

安全警告:trustXForwardedFor

security.trustXForwardedFor 選項(預設為 true,由模組設定)決定模組是否使用 X-Forwarded-For HTTP 頭來識別客戶端的 IP 地址。

  • 如果設定為 true:模組將使用 X-Forwarded-For 頭中提供的 IP 地址。當您的 Nuxt 應用程式位於受信任的反向代理、負載均衡器或 CDN(如 Nginx、Cloudflare、AWS ELB/ALB)後面,並且它們正確設定此頭以包含真實的客戶端 IP 時,這種情況很常見。
  • 警告:如果 trustXForwardedFortrue 並且您的應用程式直接面向網際網路,或者您的代理未配置為從客戶端剝離傳入的 X-Forwarded-For 頭,則惡意使用者可以透過傳送偽造的 X-Forwarded-For 頭來欺騙他們的 IP 地址。這將使他們能夠繞過速率限制或導致其他使用者被錯誤地速率限制。
  • 如果設定為 false:模組將使用傳入連線的直接 IP 地址(即 event.node.req.socket.remoteAddress)。如果您的應用程式直接面向網際網路,或者您不確定代理的配置,請使用此設定。
  • 建議:僅當您確定您的反向代理已正確配置以設定此頭並剝離任何客戶端傳送的版本時,才啟用 trustXForwardedFor: true。否則,將其設定為 false

3. 將 nitro/storage 新增到 nuxt.config.ts

您可以使用任何您想要的儲存,但您必須使用 shield 作為儲存的名稱。

{
  "nitro": {
    "storage": {
      "shield": {
        // storage name, you **must** use "shield" as the name
        "driver": "memory"
      }
    }
  }
}

例如,如果您使用 Redis,您可以使用以下配置,定義主機和埠。

{
  "nitro": {
    "storage": {
      "shield": {
        "driver": "redis",
        "host": "localhost",
        "port": 6379,
      }
    }
  }
}

4. 將清理任務新增到 nuxt.config.ts

{
  "nitro": {
    "experimental": {
      "tasks": true
    },
    "scheduledTasks": {
      "*/15 * * * *": ["shield:cleanBans"], // Example: clean expired bans every 15 minutes
      "0 0 * * *": ["shield:cleanIpData"]   // Example: clean old IP data daily at midnight
    }
  }
}

5. 建立您的清理任務

建議定期清理過期的停用和舊的 IP 跟蹤資料,以防止儲存膨脹並確保良好的效能。

a) 清理過期停用的任務

此任務在停用期過後從儲存中移除停用條目 (ban:xxx.xxx.xxx.xxx)。

server/tasks/shield/cleanBans.ts 中(您可以隨意命名檔案和任務)

import { isActualBanTimestampExpired } from '#imports'; // Auto-imported utility from nuxt-api-shield

export default defineTask({
  meta: {
    name: 'shield:cleanBans', // Match the name in scheduledTasks
    description: 'Clean expired bans from nuxt-api-shield storage.',
  },
  async run() {
    const shieldStorage = useStorage('shield'); // Use your configured storage name

    // Only fetch keys that start with the 'ban:' prefix
    const banKeys = await shieldStorage.getKeys('ban:');

    let cleanedCount = 0;
    for (const key of banKeys) {
      const bannedUntilRaw = await shieldStorage.getItem(key);
      if (isActualBanTimestampExpired(bannedUntilRaw)) {
        await shieldStorage.removeItem(key);
        cleanedCount++;
      }
    }
    console.log(`[nuxt-api-shield] Cleaned ${cleanedCount} expired ban(s).`);
    return { result: { cleanedCount } };
  },
});

isActualBanTimestampExpired 實用程式由 nuxt-api-shield 提供,並且應該透過 #imports 可用。

b) 清理舊 IP 跟蹤資料的任務

此任務清理在特定時期內不活躍(即其 time 欄位未更新)的 IP 跟蹤條目 (ip:xxx.xxx.xxx.xxx)。此時期由您的 nuxt.config.ts 中(在 nuxtApiShield 下)的 ipTTL 配置選項定義,預設為 7 天。此清理有助於防止您的儲存因很少請求但從未被停用的 IP 而無限增長。

server/tasks/shield/cleanIpData.ts

import type { RateLimit } from '#imports'; // Or from 'nuxt-api-shield/types' if made available by the module
import { useRuntimeConfig } from '#imports';

export default defineTask({
  meta: {
    name: 'shield:cleanIpData', // Match the name in scheduledTasks
    description: 'Clean old IP tracking data from nuxt-api-shield storage.',
  },
  async run() {
    const shieldStorage = useStorage('shield');
    const config = useRuntimeConfig().public.nuxtApiShield;

    // ipTTL is expected to be in seconds from config (module applies default if not set by user)
    const ipTTLseconds = config.ipTTL;

    if (!ipTTLseconds || ipTTLseconds <= 0) {
      console.log('[nuxt-api-shield] IP data cleanup (ipTTL) is disabled or invalid.');
      return { result: { cleanedCount: 0, status: 'disabled_or_invalid_ttl' } };
    }
    const ipTTLms = ipTTLseconds * 1000;

    const ipKeys = await shieldStorage.getKeys('ip:');
    const currentTime = Date.now();
    let cleanedCount = 0;

    for (const key of ipKeys) {
      const entry = await shieldStorage.getItem(key) as RateLimit | null;

      // Check if entry exists and has a numeric 'time' property
      if (entry && typeof entry.time === 'number') {
        if ((currentTime - entry.time) > ipTTLms) {
          await shieldStorage.removeItem(key);
          cleanedCount++;
        }
      } else {
        // Clean up entries that are null, not an object, or missing a numeric 'time'
        await shieldStorage.removeItem(key);
        cleanedCount++;
      }
    }

    console.log(`[nuxt-api-shield] Cleaned ${cleanedCount} old/malformed IP data entries.`);
    return { result: { cleanedCount } };
  },
});

如果您希望使用與預設值(7 天)不同的值,請確保在您的 nuxt.config.tsnuxtApiShield 下配置 ipTTL。在您的配置中將 ipTTL: 0(或任何非正數)將停用此清理任務。RateLimit 型別應該透過 #imports 可用,如果您的模組匯出它或使其可用於 Nuxt 的自動匯入系統。

重要注意事項

資料隱私(IP 地址儲存)

nuxt-api-shield 透過跟蹤 IP 地址來監控請求速率和應用停用。這意味著 IP 地址(根據 GDPR 等法規可視為個人身份資訊 (PII))由模組儲存。

  • 儲存的資料
    • ip:<IP_ADDRESS>:儲存 { count: number, time: number } 用於跟蹤請求速率。
    • ban:<IP_ADDRESS>:儲存一個時間戳,指示 IP 地址停用的到期時間。
  • 合規性:確保您的使用符合任何適用的資料隱私法規。這可能涉及更新您的隱私政策以告知使用者此資料處理。
  • 資料保留
    • 停用條目在過期後由 shield:cleanBans 任務清理。
    • IP 跟蹤條目根據 ipTTL 設定由 shield:cleanIpData 任務清理。

儲存安全

  • 檔案系統驅動程式 (driver: 'fs'):如果您將檔案系統驅動程式用於 unstorage(例如,driver: 'fs'base: '.shield'),請確保儲存目錄(如果透過 log.path 啟用日誌記錄,則包括 logs 目錄)
    • 不可透過網路訪問:您的 Web 伺服器不應配置為從這些目錄提供檔案。
    • 適當的許可權:目錄應具有適當的伺服器端檔案許可權,以防止未經授權的讀取或寫入。
  • 其他驅動程式(Redis 等):如果使用 Redis 等資料庫驅動程式,請確保您的資料庫伺服器本身是安全的(例如,身份驗證、網路訪問控制)。

錯誤訊息 (errorMessage)

模組配置中的 errorMessage 選項在 429 響應的正文中返回。

  • 建議使用純文字訊息。
  • 如果您選擇在 errorMessage 中使用 HTML,請確保您的客戶端應用程式正確清理它或以防止 XSS 漏洞的方式渲染它。模組本身不會清理此使用者配置的訊息。

開發

# Install dependencies
yarn

# Generate type stubs
yarn dev:prepare

# Develop with the playground
yarn dev

# Build the playground
yarn dev:build

# Run ESLint
yarn lint

# Run Vitest
yarn test
yarn test:watch

# Release new version
yarn release:patch
yarn release:minor