151 lines
3.9 KiB
TypeScript
151 lines
3.9 KiB
TypeScript
/**
|
||
* 速率限制中间件
|
||
* 使用内存存储(生产环境建议使用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),
|
||
|
||
// 高频只读接口(首页/画布轮询):每分钟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);
|
||
}
|