feat: 重构业务模块架构

- 新增auth模块处理认证逻辑
- 新增security模块处理安全相关功能
- 新增user-mgmt模块管理用户相关操作
- 新增shared模块存放共享组件
- 重构admin模块,添加DTO和Guards
- 为admin模块添加测试文件结构
This commit is contained in:
moyin
2025-12-24 18:04:30 +08:00
parent 85d488a508
commit 47a738067a
35 changed files with 3667 additions and 227 deletions

View File

@@ -0,0 +1,317 @@
/**
* 频率限制守卫
*
* 功能描述:
* - 实现API接口的频率限制功能
* - 基于IP地址进行限制
* - 支持自定义限制规则
*
* 使用场景:
* - 防止API滥用
* - 登录暴力破解防护
* - 验证码发送频率控制
*
* @author kiro-ai
* @version 1.0.0
* @since 2025-12-24
*/
import {
Injectable,
CanActivate,
ExecutionContext,
HttpException,
HttpStatus,
Logger
} from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { Request } from 'express';
import { THROTTLE_KEY, ThrottleConfig } from '../decorators/throttle.decorator';
/**
* 频率限制记录接口
*/
interface ThrottleRecord {
/** 请求次数 */
count: number;
/** 窗口开始时间 */
windowStart: number;
/** 最后请求时间 */
lastRequest: number;
}
/**
* 频率限制响应接口
*/
interface ThrottleResponse {
/** 请求是否成功 */
success: boolean;
/** 响应消息 */
message: string;
/** 错误代码 */
error_code: string;
/** 限制信息 */
throttle_info: {
/** 限制次数 */
limit: number;
/** 时间窗口(秒) */
window_seconds: number;
/** 当前请求次数 */
current_requests: number;
/** 重置时间 */
reset_time: string;
};
}
@Injectable()
export class ThrottleGuard implements CanActivate {
private readonly logger = new Logger(ThrottleGuard.name);
/**
* 存储频率限制记录
* Key: IP地址 + 路径
* Value: 限制记录
*/
private readonly records = new Map<string, ThrottleRecord>();
/**
* 清理过期记录的间隔(毫秒)
*/
private readonly cleanupInterval = 60000; // 1分钟
constructor(private readonly reflector: Reflector) {
// 启动定期清理任务
this.startCleanupTask();
}
/**
* 守卫检查函数
*
* @param context 执行上下文
* @returns 是否允许通过
*/
async canActivate(context: ExecutionContext): Promise<boolean> {
// 1. 获取频率限制配置
const throttleConfig = this.getThrottleConfig(context);
if (!throttleConfig) {
// 没有配置频率限制,直接通过
return true;
}
// 2. 获取请求信息
const request = context.switchToHttp().getRequest<Request>();
const key = this.generateKey(request, throttleConfig);
// 3. 检查频率限制
const isAllowed = this.checkThrottle(key, throttleConfig);
if (!isAllowed) {
// 4. 记录被限制的请求
this.logger.warn('请求被频率限制', {
operation: 'throttle_limit',
method: request.method,
url: request.url,
ip: request.ip,
userAgent: request.get('User-Agent'),
limit: throttleConfig.limit,
ttl: throttleConfig.ttl,
timestamp: new Date().toISOString()
});
// 5. 抛出频率限制异常
const record = this.records.get(key);
const resetTime = new Date(record!.windowStart + throttleConfig.ttl * 1000);
const response: ThrottleResponse = {
success: false,
message: throttleConfig.message || '请求过于频繁,请稍后再试',
error_code: 'TOO_MANY_REQUESTS',
throttle_info: {
limit: throttleConfig.limit,
window_seconds: throttleConfig.ttl,
current_requests: record!.count,
reset_time: resetTime.toISOString()
}
};
throw new HttpException(response, HttpStatus.TOO_MANY_REQUESTS);
}
return true;
}
/**
* 获取频率限制配置
*
* @param context 执行上下文
* @returns 频率限制配置或null
*/
private getThrottleConfig(context: ExecutionContext): ThrottleConfig | null {
// 从方法装饰器获取配置
const methodConfig = this.reflector.get<ThrottleConfig>(
THROTTLE_KEY,
context.getHandler()
);
if (methodConfig) {
return methodConfig;
}
// 从类装饰器获取配置
const classConfig = this.reflector.get<ThrottleConfig>(
THROTTLE_KEY,
context.getClass()
);
return classConfig || null;
}
/**
* 生成限制键
*
* @param request 请求对象
* @param config 频率限制配置
* @returns 限制键
*/
private generateKey(request: Request, config: ThrottleConfig): string {
const ip = request.ip || 'unknown';
const path = request.route?.path || request.url;
const method = request.method;
// 根据限制类型生成不同的键
if (config.type === 'user') {
// 基于用户的限制需要从JWT中获取用户ID
const userId = this.extractUserId(request);
return `user:${userId}:${method}:${path}`;
} else {
// 基于IP的限制默认
return `ip:${ip}:${method}:${path}`;
}
}
/**
* 检查频率限制
*
* @param key 限制键
* @param config 频率限制配置
* @returns 是否允许通过
*/
private checkThrottle(key: string, config: ThrottleConfig): boolean {
const now = Date.now();
const windowMs = config.ttl * 1000;
let record = this.records.get(key);
if (!record) {
// 第一次请求
this.records.set(key, {
count: 1,
windowStart: now,
lastRequest: now
});
return true;
}
// 检查是否需要重置窗口
if (now - record.windowStart >= windowMs) {
// 重置窗口
record.count = 1;
record.windowStart = now;
record.lastRequest = now;
return true;
}
// 在当前窗口内
if (record.count >= config.limit) {
// 超过限制
return false;
}
// 增加计数
record.count++;
record.lastRequest = now;
return true;
}
/**
* 从请求中提取用户ID
*
* @param request 请求对象
* @returns 用户ID
*/
private extractUserId(request: Request): string {
// 这里应该从JWT token中提取用户ID
// 简化实现使用IP作为fallback
const authHeader = request.get('Authorization');
if (authHeader && authHeader.startsWith('Bearer ')) {
try {
// 这里应该解析JWT token获取用户ID
// 简化实现返回token的hash
const token = authHeader.substring(7);
return Buffer.from(token).toString('base64').substring(0, 10);
} catch (error) {
// JWT解析失败使用IP
return request.ip || 'unknown';
}
}
return request.ip || 'unknown';
}
/**
* 启动清理任务
*/
private startCleanupTask(): void {
setInterval(() => {
this.cleanupExpiredRecords();
}, this.cleanupInterval);
}
/**
* 清理过期记录
*/
private cleanupExpiredRecords(): void {
const now = Date.now();
const maxAge = 3600000; // 1小时
for (const [key, record] of this.records.entries()) {
if (now - record.lastRequest > maxAge) {
this.records.delete(key);
}
}
}
/**
* 获取当前记录统计
*
* @returns 记录统计信息
*/
getStats() {
return {
totalRecords: this.records.size,
records: Array.from(this.records.entries()).map(([key, record]) => ({
key,
count: record.count,
windowStart: new Date(record.windowStart).toISOString(),
lastRequest: new Date(record.lastRequest).toISOString()
}))
};
}
/**
* 清除所有记录
*/
clearAllRecords(): void {
this.records.clear();
}
/**
* 清除指定键的记录
*
* @param key 限制键
*/
clearRecord(key: string): void {
this.records.delete(key);
}
}