express-brute vs express-rate-limit vs express-slow-down vs rate-limiter-flexible
Express.js 请求频率限制与暴力破解防护方案对比
express-bruteexpress-rate-limitexpress-slow-downrate-limiter-flexible类似的npm包:

Express.js 请求频率限制与暴力破解防护方案对比

express-rate-limit 是标准的请求频率限制中间件,适用于大多数通用场景。express-slow-down 通常与前者配合使用,用于在达到限制前逐渐延迟响应而非直接阻断。rate-limiter-flexible 提供更细粒度的控制和多种存储后端支持,适合复杂架构。express-brute 是早期的暴力破解防护库,但目前已不再维护,建议在新项目中避免使用。

npm下载趋势

3 年

GitHub Stars 排名

统计详情

npm包名称
下载量
Stars
大小
Issues
发布时间
License
express-brute0569-2110 年前BSD
express-rate-limit03,257146 kB105 天前MIT
express-slow-down030137.6 kB12 个月前MIT
rate-limiter-flexible03,546227 kB1114 天前ISC

Express.js 请求频率限制与暴力破解防护方案对比

在构建 Node.js 后端服务时,保护 API 免受滥用和暴力破解攻击是架构设计中的关键一环。express-bruteexpress-rate-limitexpress-slow-downrate-limiter-flexible 都是用于解决这一问题的中间件,但它们的设计理念、维护状态和适用场景有很大不同。本文将从技术实现、存储支持和维护状态三个维度进行深度对比。

🛠️ 基本配置与用法

这四个包的核心目标都是控制请求频率,但初始化的方式和配置项有所不同。

express-brute 采用类实例化的方式,通常用于特定路由(如登录接口)的暴力破解防护。

// express-brute
const ExpressBrute = require('express-brute');
const store = new ExpressBrute.MemoryStore();
const bruteforce = new ExpressBrute(store);

app.post('/login', bruteforce.prevent, (req, res) => {
  // 处理登录逻辑
});

express-rate-limit 作为全局或路由中间件使用,配置直观,专注于窗口期内的最大请求数。

// express-rate-limit
const rateLimit = require('express-rate-limit');

const limiter = rateLimit({
  windowMs: 15 * 60 * 1000,
  max: 100
});

app.use(limiter);

express-slow-down 配置类似 express-rate-limit,但重点在于设置延迟阈值,而非直接阻断。

// express-slow-down
const slowDown = require('express-slow-down');

const speedLimiter = slowDown({
  windowMs: 15 * 60 * 1000,
  delayAfter: 50,
  delayMs: (hits) => hits * 100
});

app.use(speedLimiter);

rate-limiter-flexible 需要实例化限制器对象,并配合中间件包装器使用,配置更为灵活。

// rate-limiter-flexible
const { RateLimiterMemory, RateLimiterExpress } = require('rate-limiter-flexible');

const rateLimiter = new RateLimiterMemory({
  points: 10,
  duration: 1
});

app.use(new RateLimiterExpress(rateLimiter).middleware);

💾 存储后端支持

存储后端决定了限制数据保存在哪里,直接影响系统的扩展性和性能。

express-brute 支持内存和 Redis,但配置方式较为老旧,扩展性有限。

// express-brute
const RedisStore = require('express-brute-redis');
const store = new RedisStore({
  host: 'localhost',
  port: 6379
});

express-rate-limit 默认使用内存,但官方支持多种外部存储插件(如 Redis、MongoDB)。

// express-rate-limit
const RedisStore = require('rate-limit-redis');
const limiter = rateLimit({
  store: new RedisStore({
    sendCommand: (...args) => redisClient.call(...args),
  }),
  windowMs: 15 * 60 * 1000,
  max: 100
});

express-slow-down 通常复用 express-rate-limit 的存储配置,两者常一起工作。

// express-slow-down
const RedisStore = require('rate-limit-redis');
const speedLimiter = slowDown({
  store: new RedisStore({
    sendCommand: (...args) => redisClient.call(...args),
  }),
  windowMs: 15 * 60 * 1000
});

rate-limiter-flexible 原生支持最广泛的存储后端,包括 Redis、Memcached、Postgres、Mongo 等。

// rate-limiter-flexible
const { RateLimiterRedis } = require('rate-limiter-flexible');
const rateLimiter = new RateLimiterRedis({
  storeClient: redisClient,
  points: 10,
  duration: 1
});

🔑 自定义键与粒度控制

如何识别用户(IP、UserID、API Key)决定了防护的精准度。

express-brute 默认基于 IP,自定义键需要通过中间件逻辑手动处理,较为繁琐。

// express-brute
app.post('/login', (req, res, next) => {
  bruteforce.handle(new ExpressBrute.Request(req, res), next, {
    id: req.body.username // 手动指定键
  });
}, (req, res) => {
  // 登录成功
});

express-rate-limit 通过 keyGenerator 配置项轻松自定义键,支持基于用户 ID 等逻辑。

// express-rate-limit
const limiter = rateLimit({
  keyGenerator: (req) => {
    return req.user.id || req.ip;
  }
});

express-slow-down 同样支持 keyGenerator,逻辑与 express-rate-limit 一致。

// express-slow-down
const speedLimiter = slowDown({
  keyGenerator: (req) => {
    return req.user.id || req.ip;
  }
});

rate-limiter-flexible 在中间件配置中直接支持 getKey,且支持多维度组合键,灵活性最高。

// rate-limiter-flexible
app.use(new RateLimiterExpress(rateLimiter, {
  getKey: (req) => {
    return `${req.user.id}_${req.ip}`;
  }
}).middleware);

⚠️ 维护状态与安全性

选择库时,维护状态直接关系到安全性。过时的库可能包含未修复的漏洞。

express-brute 已多年未更新,社区普遍认为其不再适合新项目。存在潜在的安全风险,不建议继续使用。

// express-brute
// ⚠️ 警告:该库已不再维护,npm 页面无近期更新
// 建议迁移至 rate-limiter-flexible

express-rate-limit 维护活跃,定期更新以适应 Express 新版本和安全需求。

// express-rate-limit
// ✅ 状态:活跃维护
// 适合大多数标准场景

express-slow-down 维护活跃,通常与 express-rate-limit 同步更新。

// express-slow-down
// ✅ 状态:活跃维护
// 适合作为补充策略

rate-limiter-flexible 维护活跃,专注于提供企业级的灵活性和安全性。

// rate-limiter-flexible
// ✅ 状态:活跃维护
// 适合复杂和高并发场景

📊 总结对比表

特性express-bruteexpress-rate-limitexpress-slow-downrate-limiter-flexible
维护状态❌ 不再维护✅ 活跃✅ 活跃✅ 活跃
主要用途暴力破解防护通用频率限制响应延迟 throttling复杂频率限制
存储支持有限 (Redis/Mem)多 (通过插件)多 (同 rate-limit)极多 (原生支持)
配置复杂度中高
推荐场景旧项目迁移新项目默认选择用户体验优化分布式/复杂系统

💡 架构建议

express-rate-limit 是大多数项目的默认选择 🧰。它简单、可靠,足以应对 90% 的 API 防护需求。配合 express-slow-down 可以在不阻断用户的情况下缓解流量峰值。

rate-limiter-flexible 是复杂系统的首选 🔧。如果你需要基于数据库用户 ID 进行限制,或者需要在 Redis 集群中共享限制状态,它的灵活性和存储支持是无与伦比的。

express-brute 应被视作技术债务 🗑️。虽然它曾经很流行,但现在的替代品更安全、更灵活。如果在新项目中看到它,请务必替换。

最终建议:对于新项目,优先使用 express-rate-limit。如果业务逻辑复杂或需要分布式支持,选择 rate-limiter-flexible。永远不要在新代码中引入 express-brute

如何选择: express-brute vs express-rate-limit vs express-slow-down vs rate-limiter-flexible

  • express-brute:

    不建议在新项目中使用。该库已多年未更新,存在潜在的安全维护风险。如果你的旧项目正在使用它,建议尽快迁移到 rate-limiter-flexibleexpress-rate-limit 以获得持续的安全补丁和支持。

  • express-rate-limit:

    选择 express-rate-limit 如果你需要快速搭建标准的频率限制功能。它配置简单,社区支持好,适合大多数 API 防护场景,尤其是只需要基于 IP 或简单键值进行限制的情况。

  • express-slow-down:

    选择 express-slow-down 当你希望改善用户体验而不是直接返回 429 错误。它适合配合 express-rate-limit 使用,在用户接近限制阈值时逐渐增加响应延迟,给合法用户缓冲空间。

  • rate-limiter-flexible:

    选择 rate-limiter-flexible 如果你需要复杂的限制逻辑,比如基于用户 ID、API 密钥或多维度组合键。它支持多种存储后端(Redis、MongoDB 等),适合高并发或分布式系统架构。

express-brute的README

express-brute

NPM Version NPM Downloads Build Status Coverage Status Dependency Status

A brute-force protection middleware for express routes that rate-limits incoming requests, increasing the delay with each request in a fibonacci-like sequence.

Installation

via npm:

  $ npm install express-brute

A Simple Example

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!');
	}
);

Classes

ExpressBrute(store, options)

  • 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 info
    • handleStoreError 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.

ExpressBrute.MemoryStore()

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 Methods

  • prevent(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 middleware
    • ignoreIP 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.

Built-in Failure Callbacks

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 stores

There 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.

A More Complex Example

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
		}
	}
);

Changelog

v1.0.1

  • BUG: Fixed an edge case where freeretries weren't being respected if app servers had slightly different times

v1.0.0

  • NEW: Updated to use Express 4.x as a peer dependency.
  • REMOVED: proxyDepth option on ExpressBrute has been removed. Use app.set('trust proxy', x) from Express 4 instead. More Info
  • REMOVED: getIPFromRequest(req) has been removed from instances, use req.ip instead.

v0.6.0

  • NEW: Added new ignoreIP option. (Thanks Magnitus-!)
  • CHANGED: .reset callbacks are now always called asyncronously, regardless of the implementation of the store (particularly effects MemoryStore).
  • CHANGED: Unit tests have been converted from Jasmine to Mocha/Chai/Sinon
  • BUG: Fixed a crash when .reset was called without a callback function

v0.5.3

  • NEW: Added the 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.
  • CHANGED: Errors thrown as a result of errors raised by the store now include the store's error as well, for debugging purposes.

v0.5.2

  • CHANGED: Stopped using res.send(status, body), as it is deprecated in express 4.x. Instead call res.status and res.send separately (Thanks marinewater!)

v0.5.1

  • BUG: When setting proxyDepth to 1, ips is never populated with proxied X-Forwarded-For IP.

v0.5.0

  • NEW: Added an additional FailTooManyRequests failure callback, that returns a 429 (TooManyRequests) error instead of 403 (Forbidden). This is a more accurate error status code.
  • NEW: All the built in failure callbacks now set the "Retry-After" header to the number of seconds until it is safe to try again. Per RFC6585
  • NEW: Documentation updated to list some known store implementations.
  • CHANGED: Default failure callback is now FailTooManyRequests. FailForbidden remains an option for backwards compatiblity.
  • CHANGED: ExpressBrute.MemcachedStore is no longer included by default, and is now available as a separate module (because there are multiple store options it doesn't really make sense to include one by default).
  • CHANGED: FailMark no longer sets returns 403 Forbidden, instead does 429 TooManyRequets.

v0.4.2

  • BUG: In some cases when no callbacks were supplied memcached would drop the request. Ensure that memcached always sees a callback even if ExpressBrute isn't given one.

v0.4.1

  • NEW: 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)
  • BUG: Lifetimes were not previously getting extended properly for instances of ExpressBrute.MemoryStore

v0.4.0

  • NEW: attachResetToRequest parameter that lets you prevent the request object being decorated
  • NEW: failCallback can be overriden by getMiddleware
  • NEW: proxyDepth option on ExpressBrute that specifies how many levels of the X-Forwarded-For header to trust (inspired by express-bouncer).
  • NEW: getIPFromRequest method that essentially allows reset to used in a similar ways as in v0.2.2. This also respects the new proxyDepth setting.
  • CHANGED: getMiddleware now takes an options object instead of the key directly.

v0.3.0

  • NEW: Support for using custom keys to group requests further (e.g. grouping login requests by username)
  • NEW: Support for middleware from multiple instances of ExpressBrute on the same route.
  • NEW: Tracking lifetime now has a reasonable default derived from the other settings for that instance of ExpressBrute
  • NEW: Keys are now hashed before saving to a store, to prevent really long key names and reduce the possibility of collisions.
  • NEW: There is now a convience method that gets attached to req 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.
  • CHANGED: Tracking lifetime is now specified on ExpressBrute instead of MemcachedStore. This also means lifetime is now supported by MemoryStore.
  • CHANGED: The function signature for ExpressBrute.reset has changed. It now requires an IP and key be passed instead of a request object.
  • IMPROVED: Efficiency for large values of freeRetries.
  • BUG: Removed a small chance of incorrectly triggering brute force protection.