如何基于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
相关产品推荐
相关产品推荐

