ai-chatbot/lib/security/rate-limiter.ts
Your Name 326707064c
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
fix(proxy): isolate image-proxy rate limiter
2026-03-05 16:18:58 +08:00

145 lines
3.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 速率限制中间件
* 使用内存存储(生产环境建议使用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),
// 文件上传接口:每分钟60次请求(独立于通用API限流)
upload: new RateLimiter(60000, 60),
// 图片代理接口:每分钟3000次请求(图片列表/画布高频加载)
imageProxy: new RateLimiter(60000, 3000),
// 高频只读接口(首页/画布轮询):每分钟1200次请求
uiPolling: new RateLimiter(60000, 1200),
// 画布保存接口:每分钟300次请求(含自动保存)
canvas: new RateLimiter(60000, 300),
// 实时画布相关接口:每分钟600次请求(配置/指标/代理)
realtime: new RateLimiter(60000, 600),
// 聊天接口限制:每分钟60次请求(避免被高频轮询接口挤占额度)
chat: new RateLimiter(60000, 60),
// 严格限制:每分钟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);
}
export function checkRateLimitByIdentifier(
identifier: string,
limiter: RateLimiter = rateLimiters.api
): { allowed: boolean; remaining: number; resetTime: number } {
return limiter.check(identifier);
}