全部学科
NodeJS全栈
nodejs
Python全栈
python
小程序首页
📅 2026-05-14 10 分钟 ✍️ juanwangdev

速率限制与防暴力破解

速率限制控制客户端请求频率,防止恶意攻击和资源滥用。

速率限制类型

类型说明适用场景
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 实现共享限流计数
  • 登录接口配合验证码、渐进延迟防止暴力破解
  • 响应中包含限流信息,帮助客户端合理重试

📝 发现内容有误?点击此处直接编辑

← 上一篇 进程管理与稳定性(PM2)
下一篇 → V8引擎与NodeJs交互
想查看更多题目和详细解析?
小程序提供完整的题库、模拟考试和详细解析
马上就来

长按或扫描二维码,立即体验

扫码体验小程序
马上就来
使用微信扫描二维码
立即体验完整题库