ai-chatbot/lib/security/rate-limiter.ts

155 lines
4.1 KiB
TypeScript
Raw Permalink 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),
// 管理后台接口:设置页会并发加载大量配置,不能和公开 API 共用低阈值
admin: new RateLimiter(60000, 3000),
// 文件上传接口:每分钟60次请求(独立于通用API限流)
upload: new RateLimiter(60000, 60),
// 原图下载需要服务端转码:按登录用户独立限流,避免共享出口 IP 互相影响
download: new RateLimiter(60000, 60),
// 图片代理接口:每分钟3000次请求(图片列表/画布高频加载)
imageProxy: new RateLimiter(60000, 3000),
// CAD replies include many previews, drawings and range requests. Keep their
// read budget separate from conversation submissions, scoped per user.
clawdArtifact: new RateLimiter(60000, 600),
// 高频只读接口(首页/画布轮询):每分钟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);
}