01MVP 标识01MVP
包文档安全速率限制

速率限制

零依赖的速率限制器,保护 API 端点防止滥用

概述

@mono/rate-limit 提供基于滑动窗口的速率限制功能,内置内存存储,支持自定义存储(如 Redis)。

安装

pnpm add @mono/rate-limit

核心概念

速率限制器通过计数每个标识(IP、用户 ID 等)在时间窗口内的请求次数来工作。超过限制的请求会被拒绝。

import { createRateLimiter } from "@mono/rate-limit";

const limiter = createRateLimiter({
  limit: 10,    // 每个窗口最多 10 次请求
  window: 60,   // 60 秒窗口
});

实战示例

1. API 路由保护

最常见的用法 — 保护 API 端点免受滥用:

app/api/data/route.ts
import { createRateLimiter, setRateLimitHeaders } from "@mono/rate-limit";

const limiter = createRateLimiter({ limit: 30, window: 60, prefix: "api" });

export async function GET(request: Request) {
  const ip = request.headers.get("x-forwarded-for") || "unknown";
  const result = await limiter.check(ip);

  const headers = new Headers();
  setRateLimitHeaders(headers, result);

  if (!result.success) {
    return new Response(JSON.stringify({ error: "请求过于频繁,请稍后再试" }), {
      status: 429,
      headers,
    });
  }

  // 正常处理请求
  return Response.json({ data: "ok" }, { headers });
}

2. 登录接口保护

对认证端点使用更严格的限制:

app/api/auth/login/route.ts
import { createRateLimiter } from "@mono/rate-limit";

const authLimiter = createRateLimiter({
  limit: 5,
  window: 300,  // 5 分钟内最多 5 次
  prefix: "auth",
});

export async function POST(request: Request) {
  const ip = request.headers.get("x-forwarded-for") || "unknown";
  const result = await authLimiter.check(ip);

  if (!result.success) {
    return Response.json(
      { error: "登录尝试过多,请 5 分钟后再试" },
      { status: 429 },
    );
  }

  // 处理登录...
}

3. 基于用户 ID 的限制

对已登录用户按用户 ID 限制:

app/api/ai/chat/route.ts
import { createRateLimiter } from "@mono/rate-limit";
import { auth } from "@mono/auth";

const aiLimiter = createRateLimiter({
  limit: 20,
  window: 3600,  // 每小时 20 次 AI 请求
  prefix: "ai",
});

export async function POST(request: Request) {
  const session = await auth();
  if (!session?.user) {
    return Response.json({ error: "未授权" }, { status: 401 });
  }

  const result = await aiLimiter.check(session.user.id);
  if (!result.success) {
    return Response.json(
      {
        error: "AI 请求额度已用完",
        remaining: result.remaining,
        resetAt: new Date(result.reset).toISOString(),
      },
      { status: 429 },
    );
  }

  // 调用 AI 服务...
}

4. 多级限制

组合多个限制器实现分层保护:

lib/rate-limiters.ts
import { createRateLimiter } from "@mono/rate-limit";

// 全局:每秒 100 请求
export const globalLimiter = createRateLimiter({
  limit: 100,
  window: 1,
  prefix: "global",
});

// API:每分钟 30 请求
export const apiLimiter = createRateLimiter({
  limit: 30,
  window: 60,
  prefix: "api",
});

// 敏感操作:每小时 10 次
export const sensitiveLimiter = createRateLimiter({
  limit: 10,
  window: 3600,
  prefix: "sensitive",
});

export async function checkRateLimit(key: string, ...limiters: typeof globalLimiter[]) {
  for (const limiter of limiters) {
    const result = await limiter.check(key);
    if (!result.success) return result;
  }
  return { success: true, limit: 0, remaining: 0, reset: 0 };
}

5. 自定义 Redis 存储

在分布式环境中使用 Redis 作为后端:

lib/redis-rate-limit.ts
import { createRateLimiter, type RateLimitStore } from "@mono/rate-limit";
import Redis from "ioredis";

class RedisStore implements RateLimitStore {
  constructor(private redis: Redis) {}

  async increment(key: string, window: number) {
    const multi = this.redis.multi();
    multi.incr(key);
    multi.ttl(key);
    const results = await multi.exec();

    const count = (results?.[0]?.[1] as number) || 0;
    const ttl = (results?.[1]?.[1] as number) || -1;

    if (ttl === -1) {
      await this.redis.expire(key, window);
    }

    return {
      count,
      reset: Date.now() + (ttl > 0 ? ttl : window) * 1000,
    };
  }

  async reset(key: string) {
    await this.redis.del(key);
  }
}

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

export const limiter = createRateLimiter({
  limit: 100,
  window: 60,
  store: new RedisStore(redis),
});

6. Next.js 中间件集成

在中间件层面统一限制:

middleware.ts
import { createRateLimiter } from "@mono/rate-limit";
import { NextResponse } from "next/server";
import type { NextRequest } from "next/server";

const limiter = createRateLimiter({ limit: 60, window: 60 });

export async function middleware(request: NextRequest) {
  if (request.nextUrl.pathname.startsWith("/api")) {
    const ip = request.headers.get("x-forwarded-for") || "127.0.0.1";
    const result = await limiter.check(ip);

    if (!result.success) {
      return NextResponse.json(
        { error: "Rate limit exceeded" },
        { status: 429 },
      );
    }
  }

  return NextResponse.next();
}

API 参考

createRateLimiter(options)

参数类型默认值说明
limitnumber-窗口内最大请求数
windownumber-时间窗口(秒)
prefixstring"rl"键前缀
storeRateLimitStoreMemoryStore存储实现

RateLimitResult

字段类型说明
successboolean是否允许请求
limitnumber配置的限制数
remainingnumber剩余请求数
resetnumber窗口重置时间戳(ms)

setRateLimitHeaders(headers, result)

设置标准 HTTP 速率限制响应头:X-RateLimit-LimitX-RateLimit-RemainingX-RateLimit-ResetRetry-After

最佳实践

生产环境建议使用 Redis 存储,内存存储仅适用于单实例部署。

  1. 按端点分组 — 不同端点使用不同限制器和前缀
  2. 标识选择 — 未登录用 IP,已登录用用户 ID
  3. 错误信息 — 返回 reset 时间让客户端知道何时重试
  4. 响应头 — 始终设置速率限制响应头

相关文档