速率限制与防暴力破解
速率限制控制客户端请求频率,防止恶意攻击和资源滥用。
速率限制类型
| 类型 | 说明 | 适用场景 |
|---|---|---|
| IP 限流 | 按客户端 IP 限制 | 通用防护 |
| 用户限流 | 按用户 ID 限制 | 登录用户 |
| 接口限流 | 按路由限制 | 敏感接口 |
| 全局限流 | 服务器整体限制 | 资源保护 |
express-rate-limit 实现
基本配置
JavaScript
const rateLimit = require('express-rate-limit');
// 全局限流
const globalLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15分钟
max: 100, // 最多100次请求
message: '请求过于频繁,请稍后再试',
standardHeaders: true,
legacyHeaders: false
});
app.use(globalLimiter);
登录接口限流
JavaScript
// 登录限流(更严格)
const loginLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15分钟
max: 5, // 最多5次尝试
skipSuccessfulRequests: true, // 成功请求不计入
handler: (req, res) => {
res.status(429).json({
error: '登录失败次数过多',
retryAfter: req.rateLimit.resetTime
});
}
});
app.post('/api/login', loginLimiter, loginHandler);
动态限流
JavaScript
const dynamicLimiter = rateLimit({
windowMs: 60 * 1000,
max: (req) => {
// VIP 用户更高限制
if (req.user?.isVIP) return 1000;
// 普通用户
return 100;
},
keyGenerator: (req) => {
// 登录用户按 ID,未登录按 IP
return req.user?.id || req.ip;
}
});
自定义存储
JavaScript
const RedisStore = require('rate-limit-redis');
const redis = require('redis');
const client = redis.createClient({
url: 'redis://localhost:6379'
});
const redisLimiter = rateLimit({
store: new RedisStore({
sendCommand: (...args) => client.sendCommand(args)
}),
windowMs: 60 * 1000,
max: 100
});
app.use(redisLimiter);
多层限流策略
全局 + 接口限流
JavaScript
// 第一层:全局限流
app.use(rateLimit({
windowMs: 60 * 1000,
max: 300
}));
// 第二层:敏感接口限流
const strictLimiter = rateLimit({
windowMs: 60 * 1000,
max: 10
});
app.post('/api/password/reset', strictLimiter, resetPassword);
app.post('/api/register', strictLimiter, register);
app.post('/api/payment', strictLimiter, payment);
IP + 用户双重限流
JavaScript
// IP 限流
const ipLimiter = rateLimit({
windowMs: 60 * 1000,
max: 100,
keyGenerator: (req) => req.ip
});
// 用户限流
const userLimiter = rateLimit({
windowMs: 60 * 1000,
max: 50,
keyGenerator: (req) => req.user?.id,
skip: (req) => !req.user // 未登录跳过
});
app.use(ipLimiter);
app.use(userLimiter);
令牌桶算法
实现令牌桶
JavaScript
class TokenBucket {
constructor(capacity, refillRate) {
this.capacity = capacity; // 桶容量
this.refillRate = refillRate; // 每秒补充令牌数
this.tokens = capacity; // 当前令牌数
this.lastRefill = Date.now();
}
refill() {
const now = Date.now();
const elapsed = (now - this.lastRefill) / 1000;
const tokensToAdd = Math.floor(elapsed * this.refillRate);
this.tokens = Math.min(this.capacity, this.tokens + tokensToAdd);
this.lastRefill = now;
}
consume(count = 1) {
this.refill();
if (this.tokens >= count) {
this.tokens -= count;
return true;
}
return false;
}
}
// 使用
const bucket = new TokenBucket(100, 10); // 容量100,每秒补充10个
if (bucket.consume()) {
// 处理请求
} else {
// 拒绝请求
}
Redis 分布式令牌桶
JavaScript
const redis = require('redis');
const client = redis.createClient();
async function tokenBucket(key, capacity, rate) {
const now = Date.now();
const bucketKey = `bucket:${key}`;
const script = `
local bucket = redis.call('HMGET', KEYS[1], 'tokens', 'lastRefill')
local tokens = tonumber(bucket[1]) or ARGV[1]
local lastRefill = tonumber(bucket[2]) or ARGV[2]
local now = ARGV[3]
local elapsed = (now - lastRefill) / 1000
local tokensToAdd = math.floor(elapsed * ARGV[4])
tokens = math.min(ARGV[1], tokens + tokensToAdd)
if tokens >= 1 then
tokens = tokens - 1
redis.call('HMSET', KEYS[1], 'tokens', tokens, 'lastRefill', now)
redis.call('PEXPIRE', KEYS[1], ARGV[5])
return 1
end
return 0
`;
return await client.eval(
script, 1, bucketKey,
capacity, now, now, rate, 60000
);
}
滑动窗口算法
实现滑动窗口
JavaScript
class SlidingWindow {
constructor(windowSize, maxRequests) {
this.windowSize = windowSize; // 窗口大小(毫秒)
this.maxRequests = maxRequests; // 最大请求数
this.requests = []; // 请求时间戳
}
allowRequest() {
const now = Date.now();
const windowStart = now - this.windowSize;
// 移除过期请求
this.requests = this.requests.filter(t => t > windowStart);
if (this.requests.length < this.maxRequests) {
this.requests.push(now);
return true;
}
return false;
}
}
// 使用
const window = new SlidingWindow(60000, 100); // 1分钟100次
if (window.allowRequest()) {
// 处理请求
}
Redis 滑动窗口
JavaScript
async function slidingWindowLimit(key, windowMs, maxRequests) {
const now = Date.now();
const windowStart = now - windowMs;
const script = `
redis.call('ZREMRANGEBYSCORE', KEYS[1], 0, ARGV[1])
local count = redis.call('ZCARD', KEYS[1])
if count < tonumber(ARGV[2]) then
redis.call('ZADD', KEYS[1], ARGV[3], ARGV[3])
redis.call('PEXPIRE', KEYS[1], ARGV[4])
return 1
end
return 0
`;
return await client.eval(
script, 1, `ratelimit:${key}`,
windowStart, maxRequests, now, windowMs
);
}
防暴力破解策略
登录保护
JavaScript
const loginAttempts = new Map();
function checkLoginAttempts(email, ip) {
const key = `${email}:${ip}`;
const attempts = loginAttempts.get(key) || { count: 0, lockUntil: 0 };
// 检查是否锁定
if (attempts.lockUntil > Date.now()) {
const remaining = Math.ceil((attempts.lockUntil - Date.now()) / 1000);
throw new Error(`账户已锁定,请${remaining}秒后重试`);
}
return {
recordFailure: () => {
attempts.count++;
// 5次失败后锁定15分钟
if (attempts.count >= 5) {
attempts.lockUntil = Date.now() + 15 * 60 * 1000;
attempts.count = 0;
}
loginAttempts.set(key, attempts);
},
reset: () => {
loginAttempts.delete(key);
}
};
}
// 登录处理
app.post('/api/login', async (req, res) => {
const { email, password } = req.body;
const attempt = checkLoginAttempts(email, req.ip);
try {
const user = await authenticateUser(email, password);
attempt.reset();
res.json({ success: true, token: generateToken(user) });
} catch (error) {
attempt.recordFailure();
res.status(401).json({ error: '登录失败' });
}
});
验证码保护
JavaScript
const captcha = require('svg-captcha');
// 登录失败3次后需要验证码
app.post('/api/login', async (req, res) => {
const { email, password, captchaText } = req.body;
const attempts = getLoginAttempts(email);
if (attempts >= 3) {
if (!captchaText || !verifyCaptcha(req.session.captcha, captchaText)) {
return res.status(400).json({ error: '验证码错误', requireCaptcha: true });
}
}
// 登录逻辑...
});
// 获取验证码
app.get('/api/captcha', (req, res) => {
const cap = captcha.create();
req.session.captcha = cap.text;
res.type('svg');
res.send(cap.data);
});
渐进式延迟
JavaScript
function getDelayTime(attempts) {
// 指数退避:1s, 2s, 4s, 8s, 16s...
return Math.min(Math.pow(2, attempts) * 1000, 60000);
}
app.post('/api/login', async (req, res) => {
const attempts = getLoginAttempts(req.ip);
const delay = getDelayTime(attempts);
// 延迟响应
await new Promise(resolve => setTimeout(resolve, delay));
// 登录逻辑...
});
分布式限流
Redis + Lua 脚本
JavaScript
const rateLimitScript = `
local key = KEYS[1]
local limit = tonumber(ARGV[1])
local window = tonumber(ARGV[2])
local current = redis.call('INCR', key)
if current == 1 then
redis.call('PEXPIRE', key, window)
end
if current > limit then
return 0
end
return 1
`;
async function distributedLimit(key, limit, windowMs) {
const result = await client.eval(
rateLimitScript, 1,
`ratelimit:${key}`,
limit, windowMs
);
return result === 1;
}
限流响应处理
标准响应
JavaScript
const limiter = rateLimit({
windowMs: 60 * 1000,
max: 100,
handler: (req, res) => {
res.setHeader('Retry-After', Math.ceil(req.rateLimit.resetTime / 1000));
res.status(429).json({
error: 'Too Many Requests',
limit: req.rateLimit.limit,
current: req.rateLimit.current,
resetTime: req.rateLimit.resetTime
});
}
});
自定义响应头
JavaScript
const limiter = rateLimit({
windowMs: 60 * 1000,
max: 100,
standardHeaders: true, // 发送 RateLimit-* 头
legacyHeaders: false, // 不发送 X-RateLimit-* 头
});
响应头示例:
text
RateLimit-Limit: 100
RateLimit-Remaining: 95
RateLimit-Reset: 1650000000
注意:限流应结合业务场景配置,避免误伤正常用户,建议提供白名单机制。
要点总结
- 使用 express-rate-limit 快速实现基本限流
- 敏感接口配置更严格的限流策略
- 分布式系统使用 Redis 实现共享限流计数
- 登录接口配合验证码、渐进延迟防止暴力破解
- 响应中包含限流信息,帮助客户端合理重试
📝 发现内容有误?点击此处直接编辑