/** * 速率限制中间件 * 使用内存存储(生产环境建议使用Redis) */ interface RateLimitStore { [key: string]: { count: number; resetTime: number; }; } class RateLimiter { private store: RateLimitStore = {}; private windowMs: number; private maxRequests: number; constructor(windowMs: number = 60000, maxRequests: number = 100) { this.windowMs = windowMs; this.maxRequests = maxRequests; // 定期清理过期记录 setInterval(() => this.cleanup(), this.windowMs); } /** * 检查是否超过速率限制 */ check(identifier: string): { allowed: boolean; remaining: number; resetTime: number } { const now = Date.now(); const record = this.store[identifier]; if (!record || now > record.resetTime) { // 创建新记录 this.store[identifier] = { count: 1, resetTime: now + this.windowMs, }; return { allowed: true, remaining: this.maxRequests - 1, resetTime: now + this.windowMs, }; } if (record.count >= this.maxRequests) { return { allowed: false, remaining: 0, resetTime: record.resetTime, }; } record.count++; return { allowed: true, remaining: this.maxRequests - record.count, resetTime: record.resetTime, }; } /** * 清理过期记录 */ private cleanup(): void { const now = Date.now(); for (const key in this.store) { if (this.store[key].resetTime < now) { delete this.store[key]; } } } } // 创建不同级别的速率限制器 export const rateLimiters = { // 通用API限制:每分钟100次请求 api: new RateLimiter(60000, 100), // 严格限制:每分钟10次请求(用于敏感操作) strict: new RateLimiter(60000, 10), // 登录限制:每分钟5次请求 auth: new RateLimiter(60000, 5), // IP限制:每分钟50次请求 ip: new RateLimiter(60000, 50), }; /** * 获取客户端标识符(IP地址) */ export function getClientIdentifier(request: Request): string { // 优先使用x-forwarded-for(考虑代理) const forwarded = request.headers.get("x-forwarded-for"); if (forwarded) { return forwarded.split(",")[0].trim(); } // 使用x-real-ip const realIP = request.headers.get("x-real-ip"); if (realIP) { return realIP; } // 降级到unknown return "unknown"; } /** * 速率限制检查函数 */ export function checkRateLimit( request: Request, limiter: RateLimiter = rateLimiters.api ): { allowed: boolean; remaining: number; resetTime: number } { const identifier = getClientIdentifier(request); return limiter.check(identifier); }