express-brute、express-limiter、express-rate-limit、express-slow-down 和 rate-limiter-flexible 都是用于保护 Node.js Express 应用免受暴力破解、API 滥用和 DDoS 攻击的中间件。它们通过限制特定 IP 或用户在一定时间内的请求频率来工作,但实现机制、存储后端支持和灵活性各不相同。express-rate-limit 是目前最流行且维护活跃的基础限流方案;rate-limiter-flexible 提供了最强大的跨框架支持和细粒度控制;express-brute 专注于暴力破解防护但已停止维护;express-limiter 是较早期的中间件实现;而 express-slow-down 则采用渐进式延迟策略而非直接拒绝请求。
在构建生产级 Express 应用时,保护 API 免受滥用和暴力破解是基本安全要求。本文深入对比五个主流限流中间件,从架构设计、存储支持、配置灵活性到实际代码实现,帮助你在技术选型时做出明智决策。
不同包对"如何处理超限请求"有不同哲学。
express-rate-limit 采用标准的令牌桶或固定窗口算法,超限后直接返回 429 状态码。
// express-rate-limit: 直接拒绝超限请求
import rateLimit from 'express-rate-limit';
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 分钟
max: 100, // 每个 IP 最多 100 个请求
message: '请求过多,请稍后再试'
});
app.use('/api/', limiter);
express-slow-down 不直接拒绝,而是随着请求频率增加逐渐延长响应时间。
// express-slow-down: 渐进式延迟
import slowDown from 'express-slow-down';
const speedLimiter = slowDown({
windowMs: 15 * 60 * 1000,
delayAfter: 50, // 50 个请求后开始延迟
delayMs: (hits) => Math.min(hits * 100, 60000) // 每次增加 100ms,最多 60 秒
});
app.use('/api/', speedLimiter);
express-brute 专为登录等敏感端点设计,使用指数退避算法防止暴力破解。
// express-brute: 指数退避防护
import ExpressBrute from 'express-brute';
import MemStore from 'express-brute-mem-store';
const store = new MemStore();
const brute = new ExpressBrute(store);
app.post('/login', brute.prevent, (req, res) => {
// 登录逻辑
res.send('登录成功');
});
express-limiter 提供基础的 Redis 驱动限流,配置相对简单直接。
// express-limiter: Redis 基础限流
import limiter from 'express-limiter';
limiter(app, {
lookup: 'connection.remoteAddress',
total: 100,
expire: 1000 * 60 * 60,
redis: redisClient
});
rate-limiter-flexible 提供最细粒度的控制,支持按用户、IP、端点等多维度组合策略。
// rate-limiter-flexible: 细粒度策略
import { RateLimiterRedis } from 'rate-limiter-flexible';
const rateLimiter = new RateLimiterRedis({
storeClient: redisClient,
keyPrefix: 'middleware',
points: 10,
duration: 1
});
app.use('/api/', (req, res, next) => {
rateLimiter.consume(req.ip)
.then(() => next())
.catch(() => res.status(429).send('Too Many Requests'));
});
存储选择直接影响限流在集群环境下的有效性。
| 包名 | 内存存储 | Redis | Memcached | 数据库 | 其他 |
|---|---|---|---|---|---|
express-brute | ✅ | ✅ (需额外包) | ❌ | ❌ | MongoDB (需额外包) |
express-limiter | ❌ | ✅ | ❌ | ❌ | ❌ |
express-rate-limit | ✅ | ✅ | ❌ | ❌ | 自定义存储 |
express-slow-down | ✅ | ✅ | ❌ | ❌ | 依赖 express-rate-limit 存储 |
rate-limiter-flexible | ✅ | ✅ | ✅ | ✅ | 15+ 种存储 |
express-rate-limit 内置内存存储,也支持 Redis 通过 rate-limit-redis 包。
// express-rate-limit: Redis 存储配置
import RedisStore from 'rate-limit-redis';
import rateLimit from 'express-rate-limit';
const limiter = rateLimit({
store: new RedisStore({
sendCommand: (...args) => redisClient.call(...args),
}),
windowMs: 15 * 60 * 1000,
max: 100
});
rate-limiter-flexible 支持最广泛的存储后端,包括 Redis、Memcached、MySQL、PostgreSQL、MongoDB 等。
// rate-limiter-flexible: Memcached 存储
import { RateLimiterMemcached } from 'rate-limiter-flexible';
const rateLimiter = new RateLimiterMemcached({
storeClient: memcachedClient,
points: 10,
duration: 1
});
express-brute 需要为不同存储安装额外的 store 包。
// express-brute: MongoDB 存储
import ExpressBrute from 'express-brute';
import MongoDBStore from 'express-brute-mongodb-store';
const store = new MongoDBStore(mongoConnection);
const brute = new ExpressBrute(store);
如何识别"谁"在发起请求决定了限流的精确度。
express-rate-limit 默认按 IP,但支持自定义键生成函数。
// express-rate-limit: 自定义键控
const limiter = rateLimit({
keyGenerator: (req) => {
return req.user ? req.user.id : req.ip;
},
max: 100
});
express-limiter 通过 lookup 配置指定键来源。
// express-limiter: 多键控支持
limiter(app, {
lookup: ['connection.remoteAddress', 'user.id'],
total: 100,
expire: 3600000
});
rate-limiter-flexible 允许在 consume 时动态指定键,最灵活。
// rate-limiter-flexible: 动态键控
app.post('/api/action', async (req, res) => {
const key = req.user ? `user:${req.user.id}` : `ip:${req.ip}`;
try {
await rateLimiter.consume(key);
res.send('操作成功');
} catch (rejRes) {
res.status(429).send('操作过于频繁');
}
});
express-brute 默认按 IP,可通过中间件参数覆盖。
// express-brute: 按用户限流
app.post('/login', brute.prevent, (req, res, next) => {
// 验证逻辑
if (valid) {
brute.reset(req, next); // 成功后重置计数
}
});
express-slow-down 继承 express-rate-limit 的键控逻辑。
// express-slow-down: 与 rate-limit 相同键控
const speedLimiter = slowDown({
keyGenerator: (req) => req.user ? req.user.id : req.ip,
delayAfter: 50,
delayMs: 500
});
rate-limiter-flexible 支持最复杂场景,如不同端点不同限制、用户等级差异化限流。
// rate-limiter-flexible: 多级限流策略
const regularLimiter = new RateLimiterRedis({ points: 10, duration: 1 });
const premiumLimiter = new RateLimiterRedis({ points: 100, duration: 1 });
app.use('/api/', (req, res, next) => {
const limiter = req.user?.isPremium ? premiumLimiter : regularLimiter;
limiter.consume(req.ip).then(() => next()).catch(() => res.status(429).send());
});
express-rate-limit 支持请求头返回限流信息,便于客户端调整。
// express-rate-limit: 返回限流头
const limiter = rateLimit({
standardHeaders: true, // 返回 RateLimit-* 头
legacyHeaders: false,
max: 100
});
express-slow-down 可配置延迟计算函数,实现非线性惩罚。
// express-slow-down: 非线性延迟
const speedLimiter = slowDown({
delayMs: (hits) => Math.pow(hits, 2) * 100 // 指数增长延迟
});
express-brute 支持请求成功后自动重置计数,适合登录场景。
// express-brute: 成功重置
app.post('/login', brute.prevent, (req, res) => {
authenticate(req.body, (err, user) => {
if (user) {
brute.reset(req); // 登录成功重置尝试次数
res.send('欢迎');
}
});
});
express-limiter 配置相对固定,适合简单场景。
// express-limiter: 基础配置
limiter(app, {
total: 100,
expire: 3600000,
lookup: 'connection.remoteAddress'
});
重要警告:express-brute 已不再积极维护,最后更新距今较久。虽然功能稳定,但存在潜在安全风险,新项目中应避免使用。
express-limiter 维护频率较低,功能基础,适合简单 Redis 限流需求。
express-rate-limit 是目前 Express 生态中最活跃维护的限流包,社区支持好,文档完善。
express-slow-down 由 express-rate-limit 同一作者维护,两者配合使用效果最佳。
rate-limiter-flexible 维护活跃,功能最强大,适合复杂企业级应用,但学习曲线稍陡。
| 特性 | express-brute | express-limiter | express-rate-limit | express-slow-down | rate-limiter-flexible |
|---|---|---|---|---|---|
| 维护状态 | ⚠️ 停止维护 | 🟡 低频维护 | 🟢 活跃维护 | 🟢 活跃维护 | 🟢 活跃维护 |
| 存储支持 | 有限 | Redis 仅 | 内存 + Redis | 内存 + Redis | 15+ 种后端 |
| 配置灵活性 | 中 | 低 | 高 | 高 | 极高 |
| 跨框架支持 | Express 仅 | Express 仅 | Express 仅 | Express 仅 | 多框架支持 |
| 暴力破解防护 | ✅ 专为设计 | ❌ | ❌ | ❌ | ✅ 可配置 |
| 渐进延迟 | ❌ | ❌ | ❌ | ✅ 核心功能 | ✅ 可配置 |
| 学习曲线 | 低 | 低 | 低 | 低 | 中 |
推荐:express-rate-limit + Redis
大多数 API 只需要基础的请求频率限制,express-rate-limit 配置简单,社区支持好,足够满足需求。
import rateLimit from 'express-rate-limit';
import RedisStore from 'rate-limit-redis';
const apiLimiter = rateLimit({
store: new RedisStore({ sendCommand: (...args) => redis.call(...args) }),
windowMs: 15 * 60 * 1000,
max: 100,
standardHeaders: true
});
app.use('/api/', apiLimiter);
推荐:rate-limiter-flexible(新项目)或 express-brute(旧项目维护)
登录端点需要更严格的防护和指数退避策略。
import { RateLimiterRedis } from 'rate-limiter-flexible';
const loginLimiter = new RateLimiterRedis({
storeClient: redisClient,
keyPrefix: 'login',
points: 5, // 5 次尝试
duration: 60 * 15 // 15 分钟内
});
app.post('/login', async (req, res) => {
try {
await loginLimiter.consume(req.ip);
// 验证逻辑
} catch (rejRes) {
res.status(429).send('尝试次数过多');
}
});
推荐:rate-limiter-flexible
如果你的应用不仅使用 Express,还涉及 Koa、Fastify 或其他框架,统一使用 rate-limiter-flexible 可以保持限流策略一致。
推荐:express-rate-limit + express-slow-down 组合
先使用 express-slow-down 减缓频繁请求者,达到硬性限制后再用 express-rate-limit 拒绝。
import slowDown from 'express-slow-down';
import rateLimit from 'express-rate-limit';
const speedLimiter = slowDown({
windowMs: 15 * 60 * 1000,
delayAfter: 50,
delayMs: 500
});
const rateLimiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 100
});
app.use('/api/', speedLimiter, rateLimiter);
推荐:rate-limiter-flexible
需要根据用户订阅等级动态调整限流策略。
const limiters = {
free: new RateLimiterRedis({ points: 10, duration: 1 }),
pro: new RateLimiterRedis({ points: 100, duration: 1 }),
enterprise: new RateLimiterRedis({ points: 1000, duration: 1 })
};
app.use('/api/', (req, res, next) => {
const tier = req.user?.subscription || 'free';
limiters[tier].consume(req.user.id)
.then(() => next())
.catch(() => res.status(429).send('超出配额'));
});
对于新项目,优先选择 express-rate-limit(简单场景)或 rate-limiter-flexible(复杂场景)。两者维护活跃,文档完善,社区支持好。
对于旧项目维护,如果已使用 express-brute 或 express-limiter 且运行稳定,可暂时保留,但建议制定迁移计划。
express-slow-down 是很好的补充工具,与 express-rate-limit 配合使用可提供更友好的限流体验。
记住:限流只是安全策略的一环,应结合认证、授权、输入验证等措施构建完整的防护体系。
仅建议在维护旧项目时使用,该包已不再积极维护。它专为防止暴力破解设计,支持指数退避策略,但缺乏现代存储后端支持。如果是新项目,请避免使用并考虑迁移到 rate-limiter-flexible。
适用于需要简单 Redis 集成的传统 Express 项目。它的配置相对直接,但功能较为基础,缺乏灵活的键控策略。如果你的项目已经依赖它且运行稳定,可以继续使用,但新项目建议评估更现代的替代方案。
选择它如果你需要简单、轻量且广泛采用的 Express 专用限流中间件。它支持内存和 Redis 存储,配置简单,适合大多数标准 API 限流场景。社区活跃,文档完善,是 Express 生态中的默认选择。
选择它当你希望在不直接拒绝请求的情况下减缓恶意流量。它通过增加响应延迟来惩罚频繁请求者,适合需要保持服务可用性但希望 discouraging 滥用的场景。通常与 express-rate-limit 配合使用。
选择它如果你需要跨框架支持(不仅限于 Express)、细粒度的限流策略或多种存储后端(Redis、Memcached、数据库等)。它提供最灵活的配置选项,适合复杂的企业级应用和微服务架构。
A brute-force protection middleware for express routes that rate-limits incoming requests, increasing the delay with each request in a fibonacci-like sequence.
via npm:
$ npm install express-brute
var ExpressBrute = require('express-brute');
var store = new ExpressBrute.MemoryStore(); // stores state locally, don't use this in production
var bruteforce = new ExpressBrute(store);
app.post('/auth',
bruteforce.prevent, // error 429 if we hit this route too often
function (req, res, next) {
res.send('Success!');
}
);
store An instance of ExpressBrute.MemoryStore or some other ExpressBrute store (see a list of known stores below).options
freeRetries The number of retires the user has before they need to start waiting (default: 2)minWait The initial wait time (in milliseconds) after the user runs out of retries (default: 500 milliseconds)maxWait The maximum amount of time (in milliseconds) between requests the user needs to wait (default: 15 minutes). The wait for a given request is determined by adding the time the user needed to wait for the previous two requests.lifetime The length of time (in seconds since the last request) to remember the number of requests that have been made by an IP. By default it will be set to maxWait * the number of attempts before you hit maxWait to discourage simply waiting for the lifetime to expire before resuming an attack. With default values this is about 6 hours.failCallback Gets called with (req, resp, next, nextValidRequestDate) when a request is rejected (default: ExpressBrute.FailForbidden)attachResetToRequest Specify whether or not a simplified reset method should be attached at req.brute.reset. The simplified method takes only a callback, and resets all ExpressBrute middleware that was called on the current request. If multiple instances of ExpressBrute have middleware on the same request, only those with attachResetToRequest set to true will be reset (default: true)refreshTimeoutOnRequest Defines whether the lifetime counts from the time of the last request that ExpressBrute didn't prevent for a given IP (true) or from of that IP's first request (false). Useful for allowing limits over fixed periods of time, for example: a limited number of requests per day. (Default: true). More infohandleStoreError Gets called whenever an error occurs with the persistent store from which ExpressBrute cannot recover. It is passed an object containing the properties message (a description of the message), parent (the error raised by the session store), and [key, ip] or [req, res, next] depending on whether or the error occurs during reset or in the middleware itself.An in-memory store for persisting request counts. Don't use this in production, instead choose one of the more robust store implementations listed below.
ExpressBrute Instance Methodsprevent(req, res, next) Middleware that will bounce requests that happen faster than
the current wait time by calling failCallback. Equivilent to getMiddleware(null)getMiddleware(options) Generates middleware that will bounce requests with the same key and IP address
that happen faster than the current wait time by calling failCallback.
Also attaches a function at req.brute.reset that can be called to reset the
counter for the current ip and key. This functions as the reset instance method,
but without the need to explicitly pass the ip and key paramters
key can be a string or alternatively it can be a function(req, res, next)
that or calls next, passing a string as the first parameter.failCallback Allows you to override the value of failCallback for this middlewareignoreIP Disregard IP address when matching requests if set to true. Defaults to false.reset(ip, key, next) Resets the wait time between requests back to its initial value. You can pass null
for key if you want to reset a request protected by protect.There are some built-in callbacks that come with BruteExpress that handle some common use cases.
ExpressBrute.FailTooManyRquests Terminates the request and responses with a 429 (Too Many Requests) error that has a Retry-After header and a JSON error message.ExpressBrute.FailForbidden Terminates the request and responds with a 403 (Forbidden) error that has a Retry-After header and a JSON error message. This is provided for compatibility with ExpressBrute versions prior to v0.5.0, for new users FailTooManyRequests is the preferred behavior.ExpressBrute.FailMark Sets res.nextValidRequestDate, the Retry-After header and the res.status=429, then calls next() to pass the request on to the appropriate routes.ExpressBrute storesThere are a number adapters that have been written to allow ExpressBrute to be used with different persistent storage implementations, some of the ones I know about include:
If you write your own store and want me to add it to the list, just drop me an email or create an issue.
require('connect-flash');
var ExpressBrute = require('express-brute'),
MemcachedStore = require('express-brute-memcached'),
moment = require('moment'),
store;
if (config.environment == 'development'){
store = new ExpressBrute.MemoryStore(); // stores state locally, don't use this in production
} else {
// stores state with memcached
store = new MemcachedStore(['127.0.0.1'], {
prefix: 'NoConflicts'
});
}
var failCallback = function (req, res, next, nextValidRequestDate) {
req.flash('error', "You've made too many failed attempts in a short period of time, please try again "+moment(nextValidRequestDate).fromNow());
res.redirect('/login'); // brute force protection triggered, send them back to the login page
};
var handleStoreError = handleStoreError: function (error) {
log.error(error); // log this error so we can figure out what went wrong
// cause node to exit, hopefully restarting the process fixes the problem
throw {
message: error.message,
parent: error.parent
};
}
// Start slowing requests after 5 failed attempts to do something for the same user
var userBruteforce = new ExpressBrute(store, {
freeRetries: 5,
minWait: 5*60*1000, // 5 minutes
maxWait: 60*60*1000, // 1 hour,
failCallback: failCallback,
handleStoreError: handleStoreError
}
});
// No more than 1000 login attempts per day per IP
var globalBruteforce = new ExpressBrute(store, {
freeRetries: 1000,
attachResetToRequest: false,
refreshTimeoutOnRequest: false,
minWait: 25*60*60*1000, // 1 day 1 hour (should never reach this wait time)
maxWait: 25*60*60*1000, // 1 day 1 hour (should never reach this wait time)
lifetime: 24*60*60, // 1 day (seconds not milliseconds)
failCallback: failCallback,
handleStoreError: handleStoreError
});
app.set('trust proxy', 1); // Don't set to "true", it's not secure. Make sure it matches your environment
app.post('/auth',
globalBruteforce.prevent,
userBruteforce.getMiddleware({
key: function(req, res, next) {
// prevent too many attempts for the same username
next(req.body.username);
}
}),
function (req, res, next) {
if (User.isValidLogin(req.body.username, req.body.password)) { // omitted for the sake of conciseness
// reset the failure counter so next time they log in they get 5 tries again before the delays kick in
req.brute.reset(function () {
res.redirect('/'); // logged in, send them to the home page
});
} else {
res.flash('error', "Invalid username or password")
res.redirect('/login'); // bad username/password, send them back to the login page
}
}
);
Express 4.x as a peer dependency.proxyDepth option on ExpressBrute has been removed. Use app.set('trust proxy', x) from Express 4 instead. More InfogetIPFromRequest(req) has been removed from instances, use req.ip instead..reset callbacks are now always called asyncronously, regardless of the implementation of the store (particularly effects MemoryStore).handleStoreError option to allow more customizable handling of errors that are thrown by the persistent store. Default behavior is to throw the errors as an exception - there is nothing ExpressBrute can do to recover.FailTooManyRequests failure callback, that returns a 429 (TooManyRequests) error instead of 403 (Forbidden). This is a more accurate error status code.FailTooManyRequests. FailForbidden remains an option for backwards compatiblity.FailMark no longer sets returns 403 Forbidden, instead does 429 TooManyRequets.refreshTimeoutOnRequest option that allows you to prevent the remaining lifetime for a timer from being reset on each request (useful for implementing limits for set time frames, e.g. requests per day)ExpressBrute.MemoryStoreattachResetToRequest parameter that lets you prevent the request object being decoratedfailCallback can be overriden by getMiddlewareproxyDepth option on ExpressBrute that specifies how many levels of the X-Forwarded-For header to trust (inspired by express-bouncer).getIPFromRequest method that essentially allows reset to used in a similar ways as in v0.2.2. This also respects the new proxyDepth setting.getMiddleware now takes an options object instead of the key directly.ExpressBrute on the same route.lifetime now has a reasonable default derived from the other settings for that instance of ExpressBrutereq object as req.brute.reset. It takes a single parameter (a callback), and will reset all the counters used by ExpressBrute middleware that was called for the current route.lifetime is now specified on ExpressBrute instead of MemcachedStore. This also means lifetime is now supported by MemoryStore.ExpressBrute.reset has changed. It now requires an IP and key be passed instead of a request object.freeRetries.