mcpField
R
rate-limiting

rate-limiting

github.com/harperaa安全19198 次安装45 周前更新
代码仓库

关于 rate-limiting

面向 Next.js API 路由的限流安全技能,提供可复用的中间件以按 IP 控制请求频率,帮助减少暴力破解、垃圾请求和高成本操作滥用。

主要功能

  1. 01每个 IP 每分钟最多 5 次请求
  2. 02内存中跟踪请求
  3. 03支持代理转发的 IP 识别
  4. 04超限时返回 HTTP 429
  5. 05提供测试端点和验证脚本

适用场景

  1. 01保护登录和密码重置接口
  2. 02限制表单及评论提交频率
  3. 03控制 AI API 等高成本操作
  4. 04验证限流窗口和超限响应

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/harperaa/secure-claude-skills --skill rate-limiting

安装后在 Claude Code 中输入/rate-limiting确认已加载。

详细介绍

速率限制 - 防止暴力破解和资源滥用

速率限制为何重要

暴力破解问题

如果没有速率限制,攻击者每秒可以尝试数千个密码。一个 6 位密码有 3.08 亿种可能组合。

没有速率限制时:

  • 每秒尝试 1,000 次 → 5 分钟内破解

启用我们的速率限制(每分钟 5 个请求)时:

  • 每分钟尝试 5 次 → 需要 117 年
真实世界中的暴力破解攻击

Zoom 凭据填充攻击(2020 年): 攻击者使用窃取的凭据发起了超过 500,000 次登录尝试。完善的速率限制本可以在最初几百次尝试内检测并阻止攻击。

WordPress 分布式攻击(2021 年): 多个 WordPress 站点遭到分布式暴力破解攻击,攻击者尝试了数百万种登录组合。没有速率限制的站点因攻击者消耗资源而导致服务器成本飙升。

资源滥用的代价

速率限制不仅能增强安全性,还能保护你的基础设施成本。如果没有速率限制:

  • 机器人可以向你的联系表单发送数千条垃圾信息
  • 攻击者可以滥用高成本操作(AI API 调用、数据库查询)
  • 你可能还没察觉,服务器账单就已暴涨

真实案例: 一家初创公司推出了一个“总结任意文章”的 AI 功能,却没有设置速率限制。一名恶意用户在几分钟内编写脚本发送了 10,000 个请求。按 AI API 的费用计算,10 分钟内产生了 9,600 美元的费用。攻击持续了 4 小时才被发现——总成本超过 200,000 美元。

我们的速率限制架构

实现特性
  • 每个 IP 地址每分钟 5 个请求 - 在易用性和安全性之间取得平衡
  • 内存跟踪 - 速度快,无数据库开销
  • 基于 IP 的识别 - 通过 x-forwarded-for 支持代理
  • HTTP 429 状态码 - 标准的“请求过多”响应
  • 共享额度 - 所有使用 withRateLimit() 的路由共享每个 IP 每分钟 5 个请求的限制
为什么是每分钟 5 个请求?

关于易用性与安全性的研究表明,合法用户很少会在一分钟内向同一端点发出超过 5 个请求。此限制:

  • ✅ 阻止自动化攻击
  • ✅ 不影响真实用户
  • ✅ 允许用户合理地重新提交表单
  • ✅ 允许用户重试以恢复错误
为什么按 IP 跟踪?
  • 每位用户都有各自的限制
  • 针对一个 IP 的攻击不会阻止其他用户
  • 遭遇分布式攻击时,会分别限制每个机器人 IP
  • 让大规模攻击难以奏效
实现文件
  • lib/withRateLimit.ts - 速率限制中间件
  • app/api/test-rate-limit/route.ts - 测试端点
  • scripts/test-rate-limit.js - 验证脚本

如何使用速率限制

基本用法

对于任何可能遭到滥用的端点:

import { NextRequest, NextResponse } from 'next/server';
import { withRateLimit } from '@/lib/withRateLimit';

async function handler(request: NextRequest) {
  // Your business logic
  return NextResponse.json({ success: true });
}

// Apply rate limiting
export const POST = withRateLimit(handler);

export const config = {
  runtime: 'nodejs',
};
与 CSRF 保护结合使用

为会更改状态的操作提供最高级别的安全性:

import { NextRequest, NextResponse } from 'next/server';
import { withRateLimit } from '@/lib/withRateLimit';
import { withCsrf } from '@/lib/withCsrf';

async function handler(request: NextRequest) {
  // Business logic
  return NextResponse.json({ success: true });
}

// Layer both protections (rate limit first, then CSRF)
export const POST = withRateLimit(withCsrf(handler));

export const config = {
  runtime: 'nodejs',
};
何时应用速率限制

✅ 始终应用于:

  • 联系/支持表单
  • 新闻简报订阅
  • 创建账户
  • 密码重置请求
  • 文件上传端点
  • 搜索端点
  • 数据导出端点
  • 任何高成本的 AI/API 操作
  • Webhook 端点
  • 评论/评价提交
  • 报告生成
  • 批量操作

❌ 通常不需要用于:

  • 静态资源请求(由 CDN 处理)
  • 只读取公开数据的简单 GET 端点
  • 健康检查端点
  • 已受身份验证速率限制保护的端点

完整示例

示例 1:带全面保护的联系表单
// app/api/contact/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { withRateLimit } from '@/lib/withRateLimit';
import { withCsrf } from '@/lib/withCsrf';
import { validateRequest } from '@/lib/validateRequest';
import { contactFormSchema } from '@/lib/validation';
import { handleApiError } from '@/lib/errorHandler';

async function contactHandler(request: NextRequest) {
  try {
    const body = await request.json();

    const validation = validateRequest(contactFormSchema, body);
    if (!validation.success) {
      return validation.response;
    }

    const { name, email, subject, message } = validation.data;

    await sendEmail({
      to: '[email protected]',
      from: email,
      subject,
      message
    });

    return NextResponse.json({ success: true });

  } catch (error) {
    return handleApiError(error, 'contact-form');
  }
}

export const POST = withRateLimit(withCsrf(contactHandler));

export const config = {
  runtime: 'nodejs',
};
示例 2:AI API 端点(费用保护)
// app/api/summarize/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { withRateLimit } from '@/lib/withRateLimit';
import { auth } from '@clerk/nextjs/server';
import { handleApiError, handleUnauthorizedError } from '@/lib/errorHandler';
import OpenAI from 'openai';

const openai = new OpenAI();

async function summarizeHandler(request: NextRequest) {
  try {
    // Require authentication for expensive operations
    const { userId } = await auth();
    if (!userId) return handleUnauthorizedError();

    const { text } = await request.json();

    // Rate limiting prevents abuse of expensive AI API
    const response = await openai.chat.completions.create({
      model: 'gpt-4',
      messages: [
        { role: 'system', content: 'Summarize the following text concisely.' },
        { role: 'user', content: text }
      ],
      max_tokens: 150
    });

    return NextResponse.json({
      summary: response.choices[0].message.content
    });

  } catch (error) {
    return handleApiError(error, 'summarize');
  }
}

// Protect expensive AI operations with rate limiting
export const POST = withRateLimit(summarizeHandler);

export const config = {
  runtime: 'nodejs',
};
示例 3:文件上传端点
// app/api/upload/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { withRateLimit } from '@/lib/withRateLimit';
import { auth } from '@clerk/nextjs/server';
import { handleApiError, handleUnauthorizedError } from '@/lib/errorHandler';

async function uploadHandler(request: NextRequest) {
  try {
    const { userId } = await auth();
    if (!userId) return handleUnauthorizedError();

    const formData = await request.formData();
    const file = formData.get('file') as File;

    if (!file) {
      return NextResponse.json(
        { error: 'No file provided' },
        { status: 400 }
      );
    }

    // Validate file size (10MB max)
    if (file.size > 10 * 1024 * 1024) {
      return NextResponse.json(
        { error: 'File too large (max 10MB)' },
        { status: 400 }
      );
    }

    // Process upload
    const uploadResult = await processFileUpload(file, userId);

    return NextResponse.json({ success: true, fileId: uploadResult.id });

  } catch (error) {
    return handleApiError(error, 'upload');
  }
}

// Prevent upload spam
export const POST = withRateLimit(uploadHandler);

export const config = {
  runtime: 'nodejs',
};

技术实现详情

速率限制器代码(lib/withRateLimit.ts)
import { NextRequest, NextResponse } from 'next/server';

// In-memory storage for rate limiting
const rateLimitStore = new Map<string, { count: number; resetAt: number }>();

const RATE_LIMIT_WINDOW = 60 * 1000; // 1 minute in milliseconds
const MAX_REQUESTS = 5;

function getClientIp(request: NextRequest): string {
  // Check for forwarded IP (when behind proxy/load balancer)
  const forwarded = request.headers.get('x-forwarded-for');
  if (forwarded) {
    return forwarded.split(',')[0].trim();
  }

  const realIp = request.headers.get('x-real-ip');
  if (realIp) {
    return realIp;
  }

  // Fallback to direct IP
  return request.ip || 'unknown';
}

function cleanupExpiredEntries() {
  const now = Date.now();
  for (const [key, value] of rateLimitStore.entries()) {
    if (now > value.resetAt) {
      rateLimitStore.delete(key);
    }
  }
}

export function withRateLimit(
  handler: (request: NextRequest) => Promise<NextResponse>
) {
  return async (request: NextRequest) => {
    // Clean up expired entries periodically
    cleanupExpiredEntries();

    const clientIp = getClientIp(request);
    const now = Date.now();

    // Get or create rate limit entry
    let rateLimitEntry = rateLimitStore.get(clientIp);

    if (!rateLimitEntry || now > rateLimitEntry.resetAt) {
      // Create new entry or reset expired one
      rateLimitEntry = {
        count: 0,
        resetAt: now + RATE_LIMIT_WINDOW
      };
      rateLimitStore.set(clientIp, rateLimitEntry);
    }

    // Increment request count
    rateLimitEntry.count++;

    // Check if limit exceeded
    if (rateLimitEntry.count > MAX_REQUESTS) {
      const retryAfter = Math.ceil((rateLimitEntry.resetAt - now) / 1000);

      return NextResponse.json(
        {
          error: 'Too many requests',
          message: `Rate limit exceeded. Please try again in ${retryAfter} seconds.`,
          retryAfter
        },
        {
          status: 429,
          headers: {
            'Retry-After': retryAfter.toString(),
            'X-RateLimit-Limit': MAX_REQUESTS.toString(),
            'X-RateLimit-Remaining': '0',
            'X-RateLimit-Reset': rateLimitEntry.resetAt.toString()
          }
        }
      );
    }

    // Add rate limit headers to response
    const response = await handler(request);

    response.headers.set('X-RateLimit-Limit', MAX_REQUESTS.toString());
    response.headers.set(
      'X-RateLimit-Remaining',
      (MAX_REQUESTS - rateLimitEntry.count).toString()
    );
    response.headers.set('X-RateLimit-Reset', rateLimitEntry.resetAt.toString());

    return response;
  };
}

客户端处理

平滑降级
async function submitForm(data: FormData) {
  try {
    const response = await fetch('/api/contact', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(data)
    });

    if (response.status === 429) {
      // Rate limited
      const result = await response.json();
      alert(`Too many requests. Please wait ${result.retryAfter} seconds.`);
      return;
    }

    if (response.ok) {
      alert('Form submitted successfully!');
    } else {
      alert('Submission failed. Please try again.');
    }

  } catch (error) {
    console.error('Error:', error);
    alert('An error occurred. Please try again.');
  }
}
使用指数退避自动重试
async function submitWithRetry(
  data: FormData,
  maxRetries = 3
): Promise<Response> {
  for (let attempt = 0; attempt < maxRetries; attempt++) {
    const response = await fetch('/api/contact', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(data)
    });

    if (response.status !== 429) {
      return response; // Success or non-rate-limit error
    }

    // Get retry-after header
    const retryAfter = parseInt(response.headers.get('Retry-After') || '60');

    if (attempt < maxRetries - 1) {
      // Wait with exponential backoff
      const delay = Math.min(retryAfter * 1000 * (2 ** attempt), 60000);
      await new Promise(resolve => setTimeout(resolve, delay));
    }
  }

  throw new Error('Max retries exceeded');
}
显示速率限制状态
'use client';

import { useState, useEffect } from 'react';

export function RateLimitStatus() {
  const [limit, setLimit] = useState({ remaining: 5, total: 5 });

  async function checkRateLimit() {
    const response = await fetch('/api/test-rate-limit');

    setLimit({
      remaining: parseInt(response.headers.get('X-RateLimit-Remaining') || '5'),
      total: parseInt(response.headers.get('X-RateLimit-Limit') || '5')
    });
  }

  return (
    <div>
      <p>Requests remaining: {limit.remaining}/{limit.total}</p>
      <button onClick={checkRateLimit}>Check Status</button>
    </div>
  );
}

攻击场景与防护

攻击 1:暴力破解登录

攻击:

# Attacker tries multiple passwords
for i in {1..1000}; do
  curl -X POST https://yourapp.com/api/login \
    -d "username=victim&password=attempt$i"
done

防护:

  • 请求 1–5:正常处理
  • 请求 6 及之后:返回 HTTP 429 并阻止
  • 攻击在尝试 5 次后停止
  • 攻击者必须等待 60 秒才能发起下一批请求
攻击 2:表单垃圾信息

攻击:

// Bot spams contact form
for (let i = 0; i < 1000; i++) {
  fetch('/api/contact', {
    method: 'POST',
    body: JSON.stringify({ message: 'spam' })
  });
}

防护:

  • 前 5 个请求:接受
  • 后续请求:返回 HTTP 429
  • 不会造成邮件泛滥
  • 不会造成数据库垃圾信息
攻击 3:资源耗尽(AI API)

攻击:

# Attacker abuses expensive AI endpoint
while true; do
  curl -X POST https://yourapp.com/api/summarize \
    -d '{"text": "very long text..."}'
done

防护:

  • 每个 IP 每分钟最多发起 5 次 AI 调用
  • 防止产生数千美元的意外 API 费用
  • 不影响合法用户
攻击 4:分布式攻击

攻击: 多个 IP 同时发动攻击

防护:

  • 分别跟踪每个 IP
  • 每个 IP 每分钟最多发起 5 个请求
  • 攻击者必须使用 1000 个 IP,才能达到每分钟 5000 个请求
  • 攻击对攻击者来说成本更高、难度更大

测试速率限制

手动测试
# Test rate limiting
for i in {1..10}; do
  echo "Request $i:"
  curl -s -o /dev/null -w "%{http_code}\n" \
    http://localhost:3000/api/test-rate-limit
  sleep 0.1
done

# Expected output:
# Request 1: 200
# Request 2: 200
# Request 3: 200
# Request 4: 200
# Request 5: 200
# Request 6: 429
# Request 7: 429
# Request 8: 429
# Request 9: 429
# Request 10: 429
自动化测试脚本
# Run the provided test script
node scripts/test-rate-limit.js

# Expected output:
# Testing Rate Limiting (5 requests/minute per IP)
# Request  1: ✓ 200 - Success
# Request  2: ✓ 200 - Success
# Request  3: ✓ 200 - Success
# Request  4: ✓ 200 - Success
# Request  5: ✓ 200 - Success
# Request  6: ✗ 429 - Too many requests
# Request  7: ✗ 429 - Too many requests
# Request  8: ✗ 429 - Too many requests
# Request  9: ✗ 429 - Too many requests
# Request 10: ✗ 429 - Too many requests
# ✓ Rate limiting is working correctly!
窗口结束后重置测试
# Make 5 requests
for i in {1..5}; do
  curl http://localhost:3000/api/test-rate-limit
done

# Wait 61 seconds
sleep 61

# Try again - should succeed
curl http://localhost:3000/api/test-rate-limit

# Expected: 200 OK (limit reset)

进阶:自定义速率限制

如果需要为不同端点设置不同限制:

// lib/withCustomRateLimit.ts
import { NextRequest, NextResponse } from 'next/server';

const rateLimitStore = new Map<string, { count: number; resetAt: number }>();

export function withCustomRateLimit(
  maxRequests: number,
  windowMs: number
) {
  return (handler: (request: NextRequest) => Promise<NextResponse>) => {
    return async (request: NextRequest) => {
      const clientIp = getClientIp(request);
      const now = Date.now();
      const key = `${clientIp}:${request.url}`;

      let entry = rateLimitStore.get(key);

      if (!entry || now > entry.resetAt) {
        entry = { count: 0, resetAt: now + windowMs };
        rateLimitStore.set(key, entry);
      }

      entry.count++;

      if (entry.count > maxRequests) {
        return NextResponse.json(
          { error: 'Too many requests' },
          { status: 429 }
        );
      }

      return handler(request);
    };
  };
}

// Usage:
// export const POST = withCustomRateLimit(10, 60000)(handler); // 10 req/min
// export const POST = withCustomRateLimit(100, 3600000)(handler); // 100 req/hour

生产环境注意事项

基于 Redis 的速率限制(生产环境推荐)

对于使用多台服务器的生产部署,请使用 Redis:

import Redis from 'ioredis';

const redis = new Redis(process.env.REDIS_URL);

export function withRedisRateLimit(
  handler: (request: NextRequest) => Promise<NextResponse>
) {
  return async (request: NextRequest) => {
    const clientIp = getClientIp(request);
    const key = `rate-limit:${clientIp}`;

    const current = await redis.incr(key);

    if (current === 1) {
      await redis.expire(key, 60); // 60 second window
    }

    if (current > 5) {
      const ttl = await redis.ttl(key);
      return NextResponse.json(
        { error: 'Too many requests', retryAfter: ttl },
        { status: 429 }
      );
    }

    return handler(request);
  };
}
记录速率限制违规情况
import { logSecurityEvent } from '@/lib/security-logger';

// In withRateLimit function, when limit exceeded:
if (rateLimitEntry.count > MAX_REQUESTS) {
  // Log potential attack
  logSecurityEvent({
    type: 'RATE_LIMIT_EXCEEDED',
    ip: clientIp,
    path: request.nextUrl.pathname,
    count: rateLimitEntry.count,
    timestamp: new Date().toISOString()
  });

  return NextResponse.json(/* ... */);
}

速率限制可防范的情况

✅ 密码暴力破解攻击 - 主要防护措施 ✅ 凭据填充攻击 - 自动化登录尝试 ✅ API 滥用和垃圾信息 - 防止机器人发送垃圾信息 ✅ 资源耗尽(DoS) - 防止服务器不堪重负 ✅ AI/付费 API 产生的过高费用 - 费用保护 ✅ 抓取和收集数据 - 减缓批量收集 ✅ 账户枚举 - 防止发现有效账户

应避免的常见错误

❌ 不要跳过对高成本操作的速率限制 ❌ 不要在未单独设置限制的情况下,用同一端点处理不同操作 ❌ 不要只依赖客户端的速率限制 ❌ 不要忘记在前端妥善处理 HTTP 429 响应 ❌ 不要将限制设得过高(失去意义)或过低(损害用户体验)

✅ 应对所有可能遭到滥用的公共端点启用速率限制 ✅ 多服务器部署的生产环境应使用 Redis ✅ 应记录速率限制违规情况,以便进行安全监控 ✅ 应提供清晰的错误消息,并说明重试等待时间 ✅ 应结合 CSRF 防护,以实现最大程度的保护

参考资料

后续步骤

  • CSRF 防护:使用 csrf-protection skill
  • 输入验证:使用 input-validation skill
  • 测试速率限制:使用 security-testing skill
  • 完整的 API 安全防护:结合速率限制 + CSRF + 验证

评论

支持 Markdown