关于 rate-limiting
面向 Next.js API 路由的限流安全技能,提供可复用的中间件以按 IP 控制请求频率,帮助减少暴力破解、垃圾请求和高成本操作滥用。
主要功能
- 01每个 IP 每分钟最多 5 次请求
- 02内存中跟踪请求
- 03支持代理转发的 IP 识别
- 04超限时返回 HTTP 429
- 05提供测试端点和验证脚本
适用场景
- 01保护登录和密码重置接口
- 02限制表单及评论提交频率
- 03控制 AI API 等高成本操作
- 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 防护,以实现最大程度的保护
参考资料
- OWASP API Security Top 10 - Unrestricted Resource Consumption:https://owasp.org/API-Security/editions/2023/en/0xa4-unrestricted-resource-consumption/
- 速率限制最佳实践:https://cloud.google.com/architecture/rate-limiting-strategies-techniques
- HTTP 429 状态码:https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/429
后续步骤
- CSRF 防护:使用
csrf-protectionskill - 输入验证:使用
input-validationskill - 测试速率限制:使用
security-testingskill - 完整的 API 安全防护:结合速率限制 + CSRF + 验证