inint
This commit is contained in:
22
module/server/koa/middleware/errorHandler.js
Normal file
22
module/server/koa/middleware/errorHandler.js
Normal file
@@ -0,0 +1,22 @@
|
||||
import * as log4js from '../../log4js.js'
|
||||
|
||||
/**
|
||||
* 统一错误处理中间件
|
||||
* 捕获所有未处理异常,规范化错误响应格式,避免泄露内部错误信息
|
||||
*/
|
||||
export default async function errorHandler(ctx, next) {
|
||||
try {
|
||||
await next()
|
||||
} catch (err) {
|
||||
log4js.koa.error(`[${ctx.method}] ${ctx.path} — ${err.message}`, err.stack || '')
|
||||
|
||||
// 已知业务错误(主动 throw new Error)直接返回消息
|
||||
if (err.status) {
|
||||
ctx.status = err.status
|
||||
ctx.body = { code: err.status, message: err.message || '请求错误' }
|
||||
} else {
|
||||
ctx.status = 500
|
||||
ctx.body = { code: 500, message: '服务器内部错误,请稍后再试!' }
|
||||
}
|
||||
}
|
||||
}
|
||||
19
module/server/koa/middleware/ipFilter.js
Normal file
19
module/server/koa/middleware/ipFilter.js
Normal file
@@ -0,0 +1,19 @@
|
||||
import config from '../../config/index.js'
|
||||
import { getClientIp } from '../../utils.js'
|
||||
|
||||
/**
|
||||
* IP 黑名单过滤中间件
|
||||
* 对所有 /api/* 请求检查,命中封禁列表时返回 403
|
||||
*/
|
||||
export default async function ipFilter(ctx, next) {
|
||||
if (!ctx.path.startsWith('/api/')) {
|
||||
return next()
|
||||
}
|
||||
const ip = getClientIp(ctx)
|
||||
if (config.account.denyIps && config.account.denyIps.includes(ip)) {
|
||||
ctx.status = 403
|
||||
ctx.body = { code: 403, message: '当前未开放访问!' }
|
||||
return
|
||||
}
|
||||
return next()
|
||||
}
|
||||
61
module/server/koa/middleware/rateLimiter.js
Normal file
61
module/server/koa/middleware/rateLimiter.js
Normal file
@@ -0,0 +1,61 @@
|
||||
import * as log4js from '../../log4js.js'
|
||||
import { getClientIp } from '../../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()
|
||||
}
|
||||
Reference in New Issue
Block a user