import * as log4js from '../../log4js.js' import { getClientIp } from '../../utils.js' /** * 简单内存限流中间件(基于滑动窗口计数) * * 默认规则: * - 注册/发验证码:每 IP 每分钟最多 5 次 * - 登录:每 IP 每分钟最多 20 次 * - 其余 /api/*:每 IP 每分钟最多 100 次 * * 生产环境建议替换为 Redis 方案以支持多实例 */ // Map const requestMap = new Map() // 配置:[path前缀/全路径, 时间窗口(ms), 最大请求数] const RULES = [ ['/api/register', 60_000, 5], ['/api/send_code', 60_000, 5], ['/api/login', 60_000, 20], ['/api/', 60_000, 200], ] // 定时清理过期记录,避免内存泄漏 setInterval(() => { const now = Date.now() for (const [key, timestamps] of requestMap.entries()) { const fresh = timestamps.filter(t => now - t < 60_000) if (fresh.length === 0) requestMap.delete(key) else requestMap.set(key, fresh) } }, 30_000) export default async function rateLimiter(ctx, next) { if (!ctx.path.startsWith('/api/')) return next() const ip = getClientIp(ctx) const now = Date.now() // 匹配第一条符合的规则 const rule = RULES.find(([prefix]) => ctx.path.startsWith(prefix)) if (!rule) return next() const [prefix, windowMs, maxReq] = rule const key = `${ip}:${prefix}` const timestamps = (requestMap.get(key) || []).filter(t => now - t < windowMs) timestamps.push(now) requestMap.set(key, timestamps) if (timestamps.length > maxReq) { log4js.koa.warn(`限流触发: ${ip} ${ctx.path} (${timestamps.length}/${maxReq})`) ctx.status = 429 ctx.body = { code: 429, message: '请求过于频繁,请稍后再试!' } return } return next() }