Files
api_user/middleware/ratelimit.js
2026-06-12 15:24:20 +08:00

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 };