272 lines
7.1 KiB
JavaScript
272 lines
7.1 KiB
JavaScript
|
const rateLimit = require('express-rate-limit');
|
||
|
const { Logger } = require('./logger');
|
||
|
|
||
|
/**
|
||
|
* 通用速率限制配置
|
||
|
*/
|
||
|
const createRateLimit = (options = {}) => {
|
||
|
const defaultOptions = {
|
||
|
windowMs: parseInt(process.env.RATE_LIMIT_WINDOW_MS) || 60000, // 1分钟
|
||
|
max: parseInt(process.env.RATE_LIMIT_MAX_REQUESTS) || 100, // 每分钟最多100个请求
|
||
|
message: {
|
||
|
success: false,
|
||
|
error: '请求过于频繁,请稍后再试',
|
||
|
code: 'RATE_LIMIT_EXCEEDED',
|
||
|
retryAfter: Math.ceil(options.windowMs / 1000) || 60
|
||
|
},
|
||
|
standardHeaders: true, // 返回标准的 `RateLimit-*` 头部
|
||
|
legacyHeaders: false, // 禁用旧的 `X-RateLimit-*` 头部
|
||
|
skip: (req) => {
|
||
|
// 跳过健康检查和API信息接口
|
||
|
return req.path === '/api/health' || req.path === '/api';
|
||
|
},
|
||
|
handler: (req, res, next, options) => {
|
||
|
Logger.warn('触发速率限制', {
|
||
|
ip: req.ip,
|
||
|
path: req.path,
|
||
|
method: req.method,
|
||
|
userAgent: req.get('User-Agent')
|
||
|
});
|
||
|
|
||
|
res.status(options.statusCode).json(options.message);
|
||
|
},
|
||
|
keyGenerator: (req) => {
|
||
|
// 基于IP地址生成限制键
|
||
|
return req.ip;
|
||
|
}
|
||
|
};
|
||
|
|
||
|
return rateLimit({
|
||
|
...defaultOptions,
|
||
|
...options
|
||
|
});
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* 全局速率限制 - 适用于所有API端点
|
||
|
*/
|
||
|
const globalRateLimit = createRateLimit({
|
||
|
windowMs: 60000, // 1分钟
|
||
|
max: 100, // 每分钟最多100个请求
|
||
|
message: {
|
||
|
success: false,
|
||
|
error: '请求过于频繁,请稍后再试',
|
||
|
code: 'RATE_LIMIT_EXCEEDED',
|
||
|
retryAfter: 60
|
||
|
}
|
||
|
});
|
||
|
|
||
|
/**
|
||
|
* 聊天API专用速率限制 - 更严格的限制
|
||
|
*/
|
||
|
const chatRateLimit = createRateLimit({
|
||
|
windowMs: 60000, // 1分钟
|
||
|
max: 20, // 每分钟最多20次聊天请求
|
||
|
message: {
|
||
|
success: false,
|
||
|
error: '聊天请求过于频繁,请稍后再试',
|
||
|
code: 'CHAT_RATE_LIMIT_EXCEEDED',
|
||
|
retryAfter: 60
|
||
|
},
|
||
|
skip: (req) => {
|
||
|
// 只对聊天相关的POST请求进行限制
|
||
|
return !(req.path.startsWith('/api/chat') && req.method === 'POST');
|
||
|
}
|
||
|
});
|
||
|
|
||
|
/**
|
||
|
* 创建对话速率限制
|
||
|
*/
|
||
|
const conversationCreateRateLimit = createRateLimit({
|
||
|
windowMs: 300000, // 5分钟
|
||
|
max: 10, // 每5分钟最多创建10个对话
|
||
|
message: {
|
||
|
success: false,
|
||
|
error: '创建对话过于频繁,请稍后再试',
|
||
|
code: 'CONVERSATION_CREATE_RATE_LIMIT_EXCEEDED',
|
||
|
retryAfter: 300
|
||
|
},
|
||
|
skip: (req) => {
|
||
|
return !(req.path === '/api/conversations' && req.method === 'POST');
|
||
|
}
|
||
|
});
|
||
|
|
||
|
/**
|
||
|
* 流式聊天专用速率限制
|
||
|
*/
|
||
|
const streamChatRateLimit = createRateLimit({
|
||
|
windowMs: 60000, // 1分钟
|
||
|
max: 10, // 每分钟最多10次流式请求
|
||
|
message: {
|
||
|
success: false,
|
||
|
error: '流式聊天请求过于频繁,请稍后再试',
|
||
|
code: 'STREAM_RATE_LIMIT_EXCEEDED',
|
||
|
retryAfter: 60
|
||
|
},
|
||
|
skip: (req) => {
|
||
|
return !(req.path === '/api/chat/stream' && req.method === 'POST');
|
||
|
}
|
||
|
});
|
||
|
|
||
|
/**
|
||
|
* 搜索API速率限制
|
||
|
*/
|
||
|
const searchRateLimit = createRateLimit({
|
||
|
windowMs: 60000, // 1分钟
|
||
|
max: 30, // 每分钟最多30次搜索
|
||
|
message: {
|
||
|
success: false,
|
||
|
error: '搜索请求过于频繁,请稍后再试',
|
||
|
code: 'SEARCH_RATE_LIMIT_EXCEEDED',
|
||
|
retryAfter: 60
|
||
|
},
|
||
|
skip: (req) => {
|
||
|
return !req.path.includes('/search');
|
||
|
}
|
||
|
});
|
||
|
|
||
|
/**
|
||
|
* 基于用户会话的速率限制
|
||
|
*/
|
||
|
const createSessionRateLimit = (options = {}) => {
|
||
|
const sessions = new Map();
|
||
|
|
||
|
return (req, res, next) => {
|
||
|
const sessionId = req.headers['x-session-id'] || req.ip;
|
||
|
const now = Date.now();
|
||
|
const windowMs = options.windowMs || 60000;
|
||
|
const maxRequests = options.max || 50;
|
||
|
|
||
|
// 清理过期的会话记录
|
||
|
for (const [id, data] of sessions.entries()) {
|
||
|
if (now - data.windowStart > windowMs) {
|
||
|
sessions.delete(id);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// 获取或创建会话记录
|
||
|
let sessionData = sessions.get(sessionId);
|
||
|
if (!sessionData || now - sessionData.windowStart > windowMs) {
|
||
|
sessionData = {
|
||
|
windowStart: now,
|
||
|
requestCount: 0
|
||
|
};
|
||
|
sessions.set(sessionId, sessionData);
|
||
|
}
|
||
|
|
||
|
// 检查是否超过限制
|
||
|
if (sessionData.requestCount >= maxRequests) {
|
||
|
Logger.warn('会话速率限制触发', {
|
||
|
sessionId,
|
||
|
requestCount: sessionData.requestCount,
|
||
|
maxRequests,
|
||
|
path: req.path
|
||
|
});
|
||
|
|
||
|
return res.status(429).json({
|
||
|
success: false,
|
||
|
error: '会话请求过于频繁,请稍后再试',
|
||
|
code: 'SESSION_RATE_LIMIT_EXCEEDED',
|
||
|
retryAfter: Math.ceil((windowMs - (now - sessionData.windowStart)) / 1000)
|
||
|
});
|
||
|
}
|
||
|
|
||
|
// 增加请求计数
|
||
|
sessionData.requestCount++;
|
||
|
|
||
|
// 设置响应头
|
||
|
res.set({
|
||
|
'X-RateLimit-Limit': maxRequests,
|
||
|
'X-RateLimit-Remaining': Math.max(0, maxRequests - sessionData.requestCount),
|
||
|
'X-RateLimit-Reset': new Date(sessionData.windowStart + windowMs).toISOString()
|
||
|
});
|
||
|
|
||
|
next();
|
||
|
};
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* 自适应速率限制 - 根据服务器负载动态调整
|
||
|
*/
|
||
|
const createAdaptiveRateLimit = (baseOptions = {}) => {
|
||
|
let currentLoad = 0;
|
||
|
|
||
|
// 定期检查服务器负载
|
||
|
setInterval(() => {
|
||
|
const usage = process.cpuUsage();
|
||
|
const memUsage = process.memoryUsage();
|
||
|
|
||
|
// 简单的负载计算(可以根据需要改进)
|
||
|
const cpuLoad = (usage.user + usage.system) / 1000000; // 转换为秒
|
||
|
const memLoad = memUsage.heapUsed / memUsage.heapTotal;
|
||
|
|
||
|
currentLoad = Math.max(cpuLoad / 100, memLoad); // 标准化到 0-1
|
||
|
}, 5000); // 每5秒检查一次
|
||
|
|
||
|
return createRateLimit({
|
||
|
...baseOptions,
|
||
|
max: (req) => {
|
||
|
const baseMax = baseOptions.max || 100;
|
||
|
|
||
|
// 根据负载调整限制
|
||
|
if (currentLoad > 0.8) {
|
||
|
return Math.floor(baseMax * 0.5); // 高负载时减少50%
|
||
|
} else if (currentLoad > 0.6) {
|
||
|
return Math.floor(baseMax * 0.7); // 中等负载时减少30%
|
||
|
} else {
|
||
|
return baseMax; // 低负载时保持原限制
|
||
|
}
|
||
|
}
|
||
|
});
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* 获取客户端真实IP地址
|
||
|
*/
|
||
|
const getClientIP = (req) => {
|
||
|
return req.headers['x-forwarded-for'] ||
|
||
|
req.headers['x-real-ip'] ||
|
||
|
req.connection.remoteAddress ||
|
||
|
req.socket.remoteAddress ||
|
||
|
(req.connection.socket ? req.connection.socket.remoteAddress : null) ||
|
||
|
req.ip;
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* IP白名单中间件
|
||
|
*/
|
||
|
const createIPWhitelist = (whitelist = []) => {
|
||
|
return (req, res, next) => {
|
||
|
const clientIP = getClientIP(req);
|
||
|
|
||
|
// 本地开发环境跳过IP检查
|
||
|
if (process.env.NODE_ENV === 'development') {
|
||
|
return next();
|
||
|
}
|
||
|
|
||
|
if (whitelist.length > 0 && !whitelist.includes(clientIP)) {
|
||
|
Logger.warn('IP不在白名单中', { clientIP, path: req.path });
|
||
|
|
||
|
return res.status(403).json({
|
||
|
success: false,
|
||
|
error: '访问被拒绝',
|
||
|
code: 'IP_NOT_WHITELISTED'
|
||
|
});
|
||
|
}
|
||
|
|
||
|
next();
|
||
|
};
|
||
|
};
|
||
|
|
||
|
module.exports = {
|
||
|
globalRateLimit,
|
||
|
chatRateLimit,
|
||
|
conversationCreateRateLimit,
|
||
|
streamChatRateLimit,
|
||
|
searchRateLimit,
|
||
|
createRateLimit,
|
||
|
createSessionRateLimit,
|
||
|
createAdaptiveRateLimit,
|
||
|
createIPWhitelist,
|
||
|
getClientIP
|
||
|
};
|