// server/src/middleware/ipRateLimit.js — 通用 IP 限流(内存版,单进程够用) // 用法:app.use('/api/ai', ipRateLimit({ windowMs: 60_000, max: 10, name: 'ai' })); // 命中时返 429 + Retry-After,不写 DB(防撞库那套是 DB 版,这里只防误触/打爆 MySQL) const buckets = new Map(); // key -> { count, resetAt } function getKey(req, name) { // 用 X-Forwarded-For 第一跳(Vite 代理会塞),fallback 到 socket 地址 const xff = (req.headers['x-forwarded-for'] || '').split(',')[0]?.trim(); const ip = xff || req.socket?.remoteAddress || req.ip || '0.0.0.0'; return `${name}:${ip}`; } export function ipRateLimit({ windowMs = 60_000, max = 10, name = 'rl' } = {}) { return function (req, res, next) { // 定时清理,避免 Map 无限增长 if (buckets.size > 5000) { const now = Date.now(); for (const [k, v] of buckets) if (v.resetAt < now) buckets.delete(k); } const key = getKey(req, name); const now = Date.now(); const b = buckets.get(key); if (!b || b.resetAt < now) { buckets.set(key, { count: 1, resetAt: now + windowMs }); return next(); } b.count++; if (b.count > max) { const retryAfter = Math.max(1, Math.ceil((b.resetAt - now) / 1000)); res.set('Retry-After', String(retryAfter)); res.set('X-RateLimit-Limit', String(max)); res.set('X-RateLimit-Remaining', '0'); res.set('X-RateLimit-Reset', String(Math.ceil(b.resetAt / 1000))); return res.status(429).json({ ok: false, error: { code: 'RATE_LIMITED', message: `请求过于频繁,请 ${retryAfter} 秒后再试`, retry_after: retryAfter, }, }); } res.set('X-RateLimit-Limit', String(max)); res.set('X-RateLimit-Remaining', String(max - b.count)); next(); }; } // 给测试用的清理函数 export function _clearBuckets() { buckets.clear(); }