52 lines
1.4 KiB
JavaScript
52 lines
1.4 KiB
JavaScript
import ResponseUtil from '../utils/api_response.js';
|
|
|
|
/**
|
|
* 内存速率限制
|
|
* 基于 IP + 接口路径维度,不引入 Redis
|
|
*/
|
|
const rateLimitStore = new Map();
|
|
|
|
// 每 60 秒清理一次过期记录
|
|
const cleanupTimer = setInterval(() => {
|
|
const now = Date.now();
|
|
for (const [key, record] of rateLimitStore.entries()) {
|
|
if (now - record.resetAt > record.windowMs) {
|
|
rateLimitStore.delete(key);
|
|
}
|
|
}
|
|
}, 60_000);
|
|
cleanupTimer.unref();
|
|
|
|
/**
|
|
* 创建速率限制中间件
|
|
* @param {number} max - 窗口内最大请求数
|
|
* @param {number} windowMs - 窗口时长(毫秒),默认 60 秒
|
|
*/
|
|
function rateLimit(max = 10, windowMs = 60_000) {
|
|
return async (ctx, next) => {
|
|
const ip = ctx.ip || ctx.request.ip;
|
|
const key = `${ip}:${ctx.path}`;
|
|
const now = Date.now();
|
|
|
|
let record = rateLimitStore.get(key);
|
|
if (!record || now - record.resetAt > windowMs) {
|
|
record = { count: 0, resetAt: now, windowMs };
|
|
rateLimitStore.set(key, record);
|
|
}
|
|
|
|
record.count++;
|
|
|
|
if (record.count > max) {
|
|
const retryAfter = Math.ceil((record.resetAt + windowMs - now) / 1000);
|
|
ctx.set('Retry-After', retryAfter);
|
|
return ResponseUtil.error(ctx, `请求过于频繁,请 ${retryAfter} 秒后重试`, null, 429);
|
|
}
|
|
|
|
ctx.set('X-RateLimit-Limit', max);
|
|
ctx.set('X-RateLimit-Remaining', Math.max(0, max - record.count));
|
|
await next();
|
|
};
|
|
}
|
|
|
|
export { rateLimit };
|