高级用法

📚 目录


不同路由的不同限制

Express 示例

const limiter = new RateLimiter({
  windowMs: 60 * 1000,
  max: 100, // 默认限制
  
  perRoute: {
    // 登录端点:15分钟最多5次尝试
    '/api/login': {
      windowMs: 15 * 60 * 1000,
      max: 5,
    },
    
    // 带路由参数的登录:每个用户ID单独限制
    // 路由参数 :id 会被匹配为正则表达式 [^/]+
    // 支持 /api/login/123, /api/login/abc 等
    '/api/login/:id': {
      windowMs: 15 * 60 * 1000,
      max: 5, // 每个用户ID的15分钟限制为5次
    },
    
    // 注册端点:24小时最多3次注册
    '/api/register': {
      windowMs: 24 * 60 * 60 * 1000,
      max: 3,
    },
    
    // 用户详情:多个路由参数
    // 支持 /api/users/123, /api/users/abc 等
    '/api/users/:id': {
      windowMs: 60000,
      max: 50,
    },
    
    // 更复杂的路由参数
    // 支持 /api/posts/123/comments/456
    '/api/posts/:postId/comments/:commentId': {
      windowMs: 60000,
      max: 30,
    },
    
    // 文件上传:1小时最多10次上传
    '/api/upload': {
      windowMs: 60 * 60 * 1000,
      max: 10,
    },
  },
});

app.use(limiter.middleware());

Egg.js 路由级别应用(最实用方案)

这是最推荐的方式 - 在路由定义时直接添加限流中间件,就像添加身份验证中间件一样!

第 1 步:创建限流中间件工厂

文件: app/middleware/rate-limit.js

const { RateLimiter } = require('flex-rate-limit');

module.exports = (app) => {
  return {
    strict: async (ctx, next) => {
      const limiter = new RateLimiter({
        windowMs: 15 * 60 * 1000,
        max: 5,
      });
      const result = await limiter.check(ctx.ip, { route: ctx.path });
      
      ctx.set('X-RateLimit-Limit', result.limit);
      ctx.set('X-RateLimit-Remaining', result.remaining);
      
      if (!result.allowed) {
        ctx.status = 429;
        ctx.body = { code: 429, message: '请求过于频繁' };
        return;
      }
      await next();
    },
    
    normal: async (ctx, next) => {
      const limiter = new RateLimiter({
        windowMs: 60 * 60 * 1000,
        max: 50,
      });
      const result = await limiter.check(ctx.ip, { route: ctx.path });
      
      if (!result.allowed) {
        ctx.status = 429;
        ctx.body = { code: 429, message: '请求过于频繁' };
        return;
      }
      await next();
    },
    
    relaxed: async (ctx, next) => {
      const limiter = new RateLimiter({
        windowMs: 60 * 1000,
        max: 200,
      });
      const result = await limiter.check(ctx.ip, { route: ctx.path });
      
      if (!result.allowed) {
        ctx.status = 429;
        ctx.body = { code: 429, message: '请求过于频繁' };
        return;
      }
      await next();
    },
    
    custom: (windowMs, max) => {
      return async (ctx, next) => {
        const limiter = new RateLimiter({ windowMs, max });
        const result = await limiter.check(ctx.ip, { route: ctx.path });
        
        if (!result.allowed) {
          ctx.status = 429;
          ctx.body = { code: 429, message: '请求过于频繁' };
          return;
        }
        await next();
      };
    },
  };
};

第 2 步:在路由中使用

文件: app/router.js

module.exports = (app) => {
  const { router, controller, middleware } = app;
  const limit = middleware.rateLimit(app);
  const baseAuth = middleware.baseAuth;

  // 认证相关 - 严格限制
  router.post('/api/login', limit.strict, controller.auth.login);
  router.post('/api/register', limit.strict, controller.auth.register);

  // 用户相关 - 宽松限制
  router.get('/api/users', limit.relaxed, controller.user.list);
  router.get('/api/users/:id', limit.relaxed, controller.user.detail);

  // 文件相关 - 中等限制
  router.post('/api/upload', baseAuth, limit.normal, controller.file.upload);

  // 特殊端点 - 自定义限制
  router.get('/sse',
    baseAuth,
    limit.custom(60 * 1000, 20),
    controller.stream.sse
  );
};

第 3 步:app.js 注册

module.exports = (app) => {
  const rateLimitFactory = require('./app/middleware/rate-limit');
  app.middleware.rateLimit = rateLimitFactory(app);
};

自定义键生成器示例

为什么需要键生成器?

键生成器决定"按什么维度限流",不同的维度有不同的效果。

键生成器对比

键生成器生成的Key示例实际效果适用场景优缺点
按IP192.168.1.1同一IP下所有用户共享限额公开API❌ 公司/网吧所有人共享
按用户user:123每个用户独立限额登录后API✅ 公平;❌ 未登录用户不限制
按路由+IP192.168.1.1:/api/login同一IP在每个路由上独立限额混合场景✅ 路由隔离;❌ 同IP用户共享
按用户+路由user:123:/api/login每个用户在每个路由上独立限额业务系统 ⭐✅ 最精细;✅ 完全隔离

实际场景对比

场景:公司网络(50个员工共享同一IP)

情况1:按IP限制

keyGenerator: keyGenerators.ip  // Key: 192.168.1.1

// 配置:1分钟100次
// 实际效果:
// - 50个员工共享100次配额
// - 员工A用了50次,其他49个人只剩50次
// ❌ 问题:互相影响,不公平

情况2:按用户限制

keyGenerator: keyGenerators.userId  // Key: user:1, user:2, user:3, ...

// 配置:1分钟100次
// 实际效果:
// - 每个员工独立100次配额
// - 员工A用了100次,不影响员工B
// ✅ 优势:公平,互不影响

情况3:按用户+路由限制(业务锁)

keyGenerator: keyGenerators.userAndRoute
// Key: user:1:/api/login, user:1:/api/data, user:2:/api/login, ...

// 配置:1分钟100次
// 实际效果:
// - 每个员工在每个接口上独立100次配额
// - 员工A在/api/login用了100次,不影响他在/api/data的配额
// - 也不影响员工B的任何配额
// ✅ 优势:最精细的控制,完全隔离

预定义键生成器详解

const { RateLimiter, keyGenerators } = require('flex-rate-limit');

// 1. 按 IP 限制(默认)
const limiter1 = new RateLimiter({
  keyGenerator: keyGenerators.ip,  // 生成Key: 192.168.1.1
});
// ✅ 适用:公开API,无需登录
// ❌ 问题:同一IP的所有用户共享限额

// 2. 按用户 ID 限制
const limiter2 = new RateLimiter({
  keyGenerator: keyGenerators.userId,  // 生成Key: user:123
});
// ✅ 适用:需要登录的API
// ⚠️ 注意:未登录用户会回退到IP限制

// 3. 按路由+IP 限制
const limiter3 = new RateLimiter({
  keyGenerator: keyGenerators.routeAndIp,  // 生成Key: 192.168.1.1:/api/login
});
// ✅ 适用:不同接口需要不同限制
// ❌ 问题:同一IP的用户仍然共享

// 4. 按API端点限制
const limiter4 = new RateLimiter({
  keyGenerator: keyGenerators.apiEndpoint,  // 生成Key: /api/v1/data:192.168.1.1
});
// ✅ 适用:RESTful API,按端点独立限制

// 5. 按用户+路由限制(业务锁,推荐)⭐
const limiter5 = new RateLimiter({
  keyGenerator: keyGenerators.userAndRoute,  // 生成Key: user:123:/api/login
});
// ✅ 适用:业务系统(推荐)
// ✅ 优势:最精细的控制,完全隔离

自定义键生成器

示例1:按IP限制

const limiter = new RateLimiter({
  keyGenerator: (req) => req.ip,
  // 生成Key: 192.168.1.1
  // 实际效果:同一IP的所有请求共享限额
});

示例2:按用户ID限制

const limiter = new RateLimiter({
  keyGenerator: (req) => {
    const userId = req.user?.id || req.ip;
    return `user:${userId}`;
    // 登录用户: user:123
    // 未登录用户: user:192.168.1.1
    // 实际效果:每个用户独立限额,未登录按IP
  },
});

示例3:按用户+路由限制(业务锁)⭐

const limiter = new RateLimiter({
  keyGenerator: (req, context) => {
    const userId = req.user?.id || req.ip;
    const route = context?.route || req.path;
    return `user:${userId}:${route}`;
    // 生成Key: user:123:/api/login
    // 实际效果:每个用户在每个路由上独立限额
  },
});

// 为什么推荐这种方式?
// ✅ 完全隔离:用户A在登录接口的限流不影响查询接口
// ✅ 公平性:不同用户互不影响
// ✅ 精确控制:可以为每个接口设置不同限制

示例4:按API密钥限制

const limiter = new RateLimiter({
  keyGenerator: (req) => {
    const apiKey = req.headers['x-api-key'] || req.query.apiKey;
    return `apikey:${apiKey || req.ip}`;
    // 有API Key: apikey:sk_1234567890
    // 无API Key: apikey:192.168.1.1
    // 实际效果:按API Key限流,适合开放平台
  },
});

键生成器选择决策树

开始

├─ 是否有用户登录系统?
│  ├─ 否 → 按IP限制(ip)
│  └─ 是 ↓

├─ 是否需要区分不同接口?
│  ├─ 否 → 按用户限制(userId)
│  └─ 是 ↓

└─ 每个用户在每个接口是否需要独立限额?
   ├─ 是 → 按用户+路由限制(userAndRoute)⭐ 推荐
   └─ 否 → 按路由+IP限制(routeAndIp)

动态限制(按用户等级)

// 根据用户等级设置不同的限制
const limiter = new RateLimiter({
  max: async (req) => {
    const user = await getUserFromRequest(req);
    
    // 不同用户等级有不同的限制
    const limits = {
      free: 100,      // 免费用户:100次/分钟
      basic: 500,     // 基础用户:500次/分钟
      premium: 5000,  // 高级用户:5000次/分钟
      enterprise: Infinity, // 企业用户:无限制
    };
    
    return limits[user?.tier || 'free'];
  },
});

自定义键生成器

const limiter = new RateLimiter({
  keyGenerator: (req) => {
    // 根据用户 ID 而不是 IP 限制速率
    // 如果未登录,使用 IP 作为备用
    if (req.user?.id) {
      return `user:${req.user.id}`;
    }
    return `ip:${req.ip}`;
  },
});

跳过条件

const limiter = new RateLimiter({
  skip: (req) => {
    // 跳过管理员用户的速率限制
    if (req.user?.role === 'admin') {
      return true;
    }
    
    // 跳过健康检查端点
    if (req.path === '/health' || req.path === '/metrics') {
      return true;
    }
    
    // 跳过内部请求(例如来自本地IP)
    if (req.ip === '127.0.0.1' || req.ip === '::1') {
      return true;
    }
    
    return false;
  },
});

手动速率限制检查

const result = await limiter.check('user-123');

if (result.allowed) {
  console.log(`请求被允许`);
  console.log(`剩余:${result.remaining}/${result.limit}`);
  console.log(`重置时间:${new Date(result.resetTime)}`);
} else {
  console.log(`超过限制`);
  console.log(`重试间隔:${result.retryAfter}ms`);
}

重置速率限制

// 重置特定键的限制计数
await limiter.reset('user-123');

// 重置所有键(仅限内存存储)
await limiter.resetAll();

📚 相关文档

深入学习

基础知识

返回