You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何基于Node.js与Express实现用户请求频率限制中间件?

实现基于用户账号的Express请求频率限制中间件

嘿,这个需求太常见了——做服务端安全防护,请求频率限制绝对是基础操作之一。咱们可以自己实现一个轻量的中间件,也可以结合现成工具优化,下面一步步来:

核心思路

基于用户的唯一标识(比如登录后的用户ID),在固定时间窗口内记录请求次数,当次数超过预设阈值时,返回429 Too Many Requests响应。关键是要选好存储方案:

  • 小型应用/测试环境:用内存存储足够简单
  • 生产环境/多实例部署:必须用Redis这类分布式存储,避免服务器重启或多节点导致计数失效

一、自定义内存版中间件(适合小型项目)

先写一个最基础的版本,用Map来存储用户的请求计数和重置时间:

// rateLimiter.js
const rateLimit = (options = {}) => {
  // 默认配置:1分钟内最多100次请求
  const { windowMs = 60 * 1000, max = 100 } = options;
  // 存储用户请求记录:key=userId,value={count: 请求次数, resetTime: 窗口重置时间戳}
  const userRequests = new Map();

  return (req, res, next) => {
    // 假设用户已通过认证,req.user中包含用户ID(比如从JWT解析而来)
    const userId = req.user?.id;
    if (!userId) {
      // 未登录用户的处理:可以放行,或者换成IP限制(req.ip),根据你的需求调整
      return next();
    }

    const now = Date.now();
    const userRecord = userRequests.get(userId);

    if (!userRecord) {
      // 用户第一次请求,初始化记录
      userRequests.set(userId, { count: 1, resetTime: now + windowMs });
      return next();
    }

    // 检查是否超出当前时间窗口
    if (now > userRecord.resetTime) {
      // 重置计数和窗口时间
      userRequests.set(userId, { count: 1, resetTime: now + windowMs });
      return next();
    }

    // 检查是否超过请求阈值
    if (userRecord.count >= max) {
      const timeLeft = Math.ceil((userRecord.resetTime - now) / 1000);
      return res.status(429).json({
        message: `请求过于频繁,请${timeLeft}秒后再试`,
        retryAfter: timeLeft
      });
    }

    // 计数加1,继续处理请求
    userRecord.count += 1;
    userRequests.set(userId, userRecord);
    next();
  };
};

module.exports = rateLimit;

如何使用

在你的Express应用中引入并挂载这个中间件,可以全局应用,也可以针对特定路由:

const express = require('express');
const rateLimit = require('./rateLimiter');
const app = express();

// 先挂载用户认证中间件(确保req.user.id存在)
app.use(require('./yourAuthMiddleware'));

// 全局应用限制:15分钟内最多300次请求
app.use(rateLimit({
  windowMs: 15 * 60 * 1000,
  max: 300
}));

// 或者给单个路由设置更严格的限制:比如用户资料接口1分钟内最多10次请求
app.get('/api/user/profile', rateLimit({ windowMs: 60 * 1000, max: 10 }), (req, res) => {
  res.json({ profile: req.user });
});

二、生产环境优化:Redis版中间件

内存存储在服务器重启或多实例部署时会丢失计数,所以生产环境建议用Redis。这里用ioredis库来实现:

// redisRateLimiter.js
const Redis = require('ioredis');
// 初始化Redis连接(根据你的实际配置调整)
const redis = new Redis({
  host: 'localhost',
  port: 6379,
  password: 'your-redis-password' // 如果有密码的话
});

const rateLimit = (options = {}) => {
  const { windowMs = 60 * 1000, max = 100 } = options;
  const keyPrefix = 'rate-limit:'; // Redis键前缀,避免和其他业务键冲突

  return async (req, res, next) => {
    const userId = req.user?.id;
    if (!userId) return next();

    const redisKey = `${keyPrefix}${userId}`;
    const now = Date.now();

    try {
      // 获取当前用户的请求计数
      const currentCount = await redis.get(redisKey);

      if (!currentCount) {
        // 第一次请求:设置计数为1,并设置过期时间(和时间窗口一致)
        await redis.set(redisKey, 1, 'PX', windowMs);
        return next();
      }

      if (parseInt(currentCount) >= max) {
        // 获取剩余过期时间,告诉客户端什么时候可以重试
        const remainingSeconds = await redis.ttl(redisKey);
        return res.status(429).json({
          message: `请求过于频繁,请${remainingSeconds}秒后再试`,
          retryAfter: remainingSeconds
        });
      }

      // 计数加1
      await redis.incr(redisKey);
      next();
    } catch (error) {
      // Redis出错时,建议放行请求(避免影响业务),同时记录错误日志
      console.error('Rate limiter Redis error:', error);
      next();
    }
  };
};

module.exports = rateLimit;

关键注意事项

  • 可靠的用户标识:一定要用后端验证后的用户ID(比如JWT解析出的ID),绝对不能用客户端传的参数,防止伪造用户绕过限制。
  • 未登录用户处理:如果需要限制未登录请求,可以用req.ip作为标识,但要注意代理环境下的真实IP获取——需要先配置app.set('trust proxy', true),这样req.ip才会是客户端的真实IP。
  • HTTP规范兼容:返回429时记得添加Retry-After响应头,方便客户端自动重试。
  • 路由差异化配置:不同的路由可以设置不同的限制阈值,比如登录接口可以设置更严格的限制(比如1分钟内5次),防止暴力破解。

内容的提问来源于stack exchange,提问作者BenFarhat Souhaib

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.19 07:30:40