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