Files
chuanqi-qycq-web/module/server/koa/middleware/rateLimiter.js
艾贤凌 d676faa704 docs(migration): 添加清渊传奇PHP到Vue+Node.js移植计划文档
- 新增 MIGRATION.md 详细记录PHP到Node.js架构迁移方案
- 包含项目背景、现有资产盘点、架构设计、任务清单等完整规划
- 记录后端补全、前端补全、PHP停用、部署运维四个阶段实施计划
- 提供技术决策、数据库说明、进度总览等关键信息
- 更新 .gitignore 添加 *_out.txt build_output.txt 构建输出文件过滤
- 修复 utils.js 路径引用问题确保代码正常运行
2026-04-24 17:57:06 +08:00

62 lines
1.8 KiB
JavaScript

import * as log4js from '../../log4js.js'
import { getClientIp } from '../../utils/utils.js'
/**
* 简单内存限流中间件(基于滑动窗口计数)
*
* 默认规则:
* - 注册/发验证码:每 IP 每分钟最多 5 次
* - 登录:每 IP 每分钟最多 20 次
* - 其余 /api/*:每 IP 每分钟最多 100 次
*
* 生产环境建议替换为 Redis 方案以支持多实例
*/
// Map<ip+path, [timestamps...]>
const requestMap = new Map()
// 配置:[path前缀/全路径, 时间窗口(ms), 最大请求数]
const RULES = [
['/api/register', 60_000, 5],
['/api/send_code', 60_000, 5],
['/api/login', 60_000, 20],
['/api/', 60_000, 200],
]
// 定时清理过期记录,避免内存泄漏
setInterval(() => {
const now = Date.now()
for (const [key, timestamps] of requestMap.entries()) {
const fresh = timestamps.filter(t => now - t < 60_000)
if (fresh.length === 0) requestMap.delete(key)
else requestMap.set(key, fresh)
}
}, 30_000)
export default async function rateLimiter(ctx, next) {
if (!ctx.path.startsWith('/api/')) return next()
const ip = getClientIp(ctx)
const now = Date.now()
// 匹配第一条符合的规则
const rule = RULES.find(([prefix]) => ctx.path.startsWith(prefix))
if (!rule) return next()
const [prefix, windowMs, maxReq] = rule
const key = `${ip}:${prefix}`
const timestamps = (requestMap.get(key) || []).filter(t => now - t < windowMs)
timestamps.push(now)
requestMap.set(key, timestamps)
if (timestamps.length > maxReq) {
log4js.koa.warn(`限流触发: ${ip} ${ctx.path} (${timestamps.length}/${maxReq})`)
ctx.status = 429
ctx.body = { code: 429, message: '请求过于频繁,请稍后再试!' }
return
}
return next()
}