forked from datawhale/whale-town-end
refactor:项目架构重构和命名规范化
- 统一文件命名为snake_case格式(kebab-case snake_case) - 重构zulip模块为zulip_core,明确Core层职责 - 重构user-mgmt模块为user_mgmt,统一命名规范 - 调整模块依赖关系,优化架构分层 - 删除过时的文件和目录结构 - 更新相关文档和配置文件 本次重构涉及大量文件重命名和模块重组, 旨在建立更清晰的项目架构和统一的命名规范。
This commit is contained in:
143
src/core/security_core/README.md
Normal file
143
src/core/security_core/README.md
Normal file
@@ -0,0 +1,143 @@
|
||||
# SecurityCore 核心安全模块
|
||||
|
||||
SecurityCore 是应用的核心安全防护模块,提供系统级的安全防护功能,包括频率限制、超时控制、内容类型验证和维护模式管理,具备完整的监控日志和配置化设计能力。
|
||||
|
||||
## 频率限制功能
|
||||
|
||||
### Throttle()
|
||||
频率限制装饰器,支持基于IP和用户的多层次限制策略,防止API滥用和暴力攻击。
|
||||
|
||||
### canActivate()
|
||||
守卫检查方法,实现频率限制的核心逻辑,支持时间窗口和计数管理。
|
||||
|
||||
### getStats()
|
||||
获取频率限制的实时统计信息,用于监控和调试。
|
||||
|
||||
### clearAllRecords()
|
||||
清除所有频率限制记录,用于管理和重置。
|
||||
|
||||
### clearRecord()
|
||||
清除指定键的频率限制记录,用于精确管理。
|
||||
|
||||
## 超时控制功能
|
||||
|
||||
### Timeout()
|
||||
超时装饰器,为API接口添加超时控制,防止长时间运行的请求阻塞系统。
|
||||
|
||||
### intercept()
|
||||
拦截器处理方法,实现超时控制逻辑和异常处理。
|
||||
|
||||
## 内容类型验证功能
|
||||
|
||||
### use()
|
||||
中间件处理方法,验证POST/PUT请求的Content-Type头,确保API接收正确的数据格式。
|
||||
|
||||
### getSupportedTypes()
|
||||
获取当前支持的Content-Type列表。
|
||||
|
||||
### addSupportedType()
|
||||
动态添加支持的Content-Type类型。
|
||||
|
||||
### addExcludePath()
|
||||
添加不需要验证Content-Type的路径规则。
|
||||
|
||||
## 维护模式管理功能
|
||||
|
||||
### use()
|
||||
中间件处理方法,检查系统维护模式状态,在维护期间阻止用户访问。
|
||||
|
||||
### isMaintenanceEnabled()
|
||||
检查维护模式是否启用。
|
||||
|
||||
### getMaintenanceInfo()
|
||||
获取完整的维护配置信息,包括开始时间、结束时间和原因。
|
||||
|
||||
## 使用的项目内部依赖
|
||||
|
||||
### ThrottleConfig (本模块)
|
||||
频率限制配置接口,定义限制次数、时间窗口、限制类型和错误消息。
|
||||
|
||||
### TimeoutConfig (本模块)
|
||||
超时配置接口,定义超时时间、错误消息和日志记录选项。
|
||||
|
||||
### ThrottlePresets (本模块)
|
||||
预定义的频率限制配置常量,包含登录、注册、验证码等常用场景的限制模板。
|
||||
|
||||
### TimeoutPresets (本模块)
|
||||
预定义的超时配置常量,包含快速操作、文件处理、数据库查询等场景的超时模板。
|
||||
|
||||
### THROTTLE_KEY (本模块)
|
||||
频率限制元数据键常量,用于装饰器元数据存储。
|
||||
|
||||
### TIMEOUT_KEY (本模块)
|
||||
超时元数据键常量,用于装饰器元数据存储。
|
||||
|
||||
### @nestjs/common (来自 NestJS框架)
|
||||
提供装饰器、异常处理、日志记录等核心功能支持。
|
||||
|
||||
### @nestjs/core (来自 NestJS框架)
|
||||
提供反射器、全局守卫和拦截器注册功能。
|
||||
|
||||
### @nestjs/config (来自 NestJS框架)
|
||||
提供配置服务,用于读取环境变量和应用配置。
|
||||
|
||||
### @nestjs/swagger (来自 NestJS框架)
|
||||
提供API文档生成和响应模式定义功能。
|
||||
|
||||
### express (来自 Express框架)
|
||||
提供HTTP请求响应对象的类型定义。
|
||||
|
||||
### rxjs (来自 RxJS库)
|
||||
提供响应式编程操作符,用于超时控制和异常处理。
|
||||
|
||||
## 核心特性
|
||||
|
||||
### 多层次安全防护
|
||||
- 频率限制:支持基于IP和用户的双重限制策略,防止API滥用和暴力攻击
|
||||
- 超时控制:防止长时间运行请求占用系统资源,提升系统稳定性
|
||||
- 内容验证:确保API接收符合规范的数据格式,防止格式错误
|
||||
- 维护模式:提供系统维护期间的访问控制,支持优雅的服务中断
|
||||
|
||||
### 配置化设计
|
||||
- 装饰器配置:支持方法级和类级的灵活配置方式,使用简单直观
|
||||
- 预设模板:提供常用安全场景的预定义配置,开箱即用
|
||||
- 环境变量:支持通过环境变量进行动态配置,适应不同部署环境
|
||||
- 运行时调整:支持动态添加规则和排除路径,无需重启服务
|
||||
|
||||
### 监控和日志
|
||||
- 详细日志:记录所有安全事件、异常情况和性能指标,便于问题排查
|
||||
- 统计信息:提供频率限制的实时统计和历史数据,支持监控分析
|
||||
- 错误追踪:完整的错误信息记录和上下文保存,提升调试效率
|
||||
- 性能监控:记录请求处理时间和资源使用情况,优化系统性能
|
||||
|
||||
### 高可用设计
|
||||
- 内存管理:自动清理过期记录,防止内存泄漏和资源浪费
|
||||
- 异常处理:完善的异常捕获和恢复机制,保证系统稳定运行
|
||||
- 资源清理:组件销毁时自动清理定时器和资源,避免资源泄漏
|
||||
- 降级策略:配置缺失时的默认行为和安全降级,保证基本功能
|
||||
|
||||
## 潜在风险
|
||||
|
||||
### 内存使用风险
|
||||
- 频率限制记录存储在内存中,高并发场景可能占用大量内存资源
|
||||
- 大量并发请求时清理任务可能影响系统性能和响应时间
|
||||
- 应用重启后所有限制记录会丢失,可能导致限制策略失效
|
||||
- 建议监控内存使用情况,考虑使用Redis等外部存储方案
|
||||
|
||||
### 配置管理风险
|
||||
- 错误的频率限制配置可能导致正常用户被误限,影响用户体验
|
||||
- 维护模式配置错误可能导致服务长时间不可用,影响业务连续性
|
||||
- 超时配置过短可能导致正常请求被误杀,过长则失去保护作用
|
||||
- 建议提供配置验证机制和紧急恢复方案,定期检查配置合理性
|
||||
|
||||
### 单点故障风险
|
||||
- 内存存储的限制记录在应用重启后会丢失,无法保持状态连续性
|
||||
- 依赖单一应用实例的状态管理,不适合分布式部署和负载均衡
|
||||
- 配置服务异常可能导致安全功能失效,存在安全隐患
|
||||
- 建议在生产环境使用持久化存储和分布式状态管理方案
|
||||
|
||||
### 性能瓶颈风险
|
||||
- 高频率的限制检查可能成为请求处理的性能瓶颈,影响系统吞吐量
|
||||
- 复杂的正则表达式匹配可能影响中间件处理速度,增加延迟
|
||||
- 频繁的日志记录在高并发场景下可能影响系统性能
|
||||
- 建议进行性能测试和优化,使用缓存减少重复计算,合理设置日志级别
|
||||
122
src/core/security_core/content_type.middleware.spec.ts
Normal file
122
src/core/security_core/content_type.middleware.spec.ts
Normal file
@@ -0,0 +1,122 @@
|
||||
/**
|
||||
* ContentTypeMiddleware 单元测试
|
||||
*
|
||||
* @author moyin
|
||||
* @version 1.0.0
|
||||
* @since 2026-01-07
|
||||
*/
|
||||
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { Request, Response, NextFunction } from 'express';
|
||||
import { ContentTypeMiddleware } from './content_type.middleware';
|
||||
|
||||
describe('ContentTypeMiddleware', () => {
|
||||
let middleware: ContentTypeMiddleware;
|
||||
let mockRequest: Partial<Request>;
|
||||
let mockResponse: Partial<Response>;
|
||||
let mockNext: NextFunction;
|
||||
|
||||
beforeEach(async () => {
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [ContentTypeMiddleware],
|
||||
}).compile();
|
||||
|
||||
middleware = module.get<ContentTypeMiddleware>(ContentTypeMiddleware);
|
||||
|
||||
mockResponse = {
|
||||
status: jest.fn().mockReturnThis(),
|
||||
json: jest.fn().mockReturnThis(),
|
||||
};
|
||||
|
||||
mockNext = jest.fn();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('use', () => {
|
||||
it('should call next() for GET requests', () => {
|
||||
// Arrange
|
||||
mockRequest = {
|
||||
method: 'GET',
|
||||
url: '/api/test',
|
||||
};
|
||||
|
||||
// Act
|
||||
middleware.use(mockRequest as Request, mockResponse as Response, mockNext);
|
||||
|
||||
// Assert
|
||||
expect(mockNext).toHaveBeenCalled();
|
||||
expect(mockResponse.status).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should call next() for excluded paths', () => {
|
||||
// Arrange
|
||||
mockRequest = {
|
||||
method: 'POST',
|
||||
url: '/api-docs/swagger',
|
||||
get: jest.fn(),
|
||||
};
|
||||
|
||||
// Act
|
||||
middleware.use(mockRequest as Request, mockResponse as Response, mockNext);
|
||||
|
||||
// Assert
|
||||
expect(mockNext).toHaveBeenCalled();
|
||||
expect(mockResponse.status).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should return 415 when Content-Type is missing', () => {
|
||||
// Arrange
|
||||
mockRequest = {
|
||||
method: 'POST',
|
||||
url: '/api/test',
|
||||
get: jest.fn().mockReturnValue(undefined),
|
||||
ip: '127.0.0.1',
|
||||
};
|
||||
|
||||
// Act
|
||||
middleware.use(mockRequest as Request, mockResponse as Response, mockNext);
|
||||
|
||||
// Assert
|
||||
expect(mockResponse.status).toHaveBeenCalledWith(415);
|
||||
expect(mockNext).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should call next() for supported Content-Type', () => {
|
||||
// Arrange
|
||||
mockRequest = {
|
||||
method: 'POST',
|
||||
url: '/api/test',
|
||||
get: jest.fn().mockImplementation((header) => {
|
||||
if (header === 'Content-Type') return 'application/json';
|
||||
return undefined;
|
||||
}),
|
||||
};
|
||||
|
||||
// Act
|
||||
middleware.use(mockRequest as Request, mockResponse as Response, mockNext);
|
||||
|
||||
// Assert
|
||||
expect(mockNext).toHaveBeenCalled();
|
||||
expect(mockResponse.status).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getSupportedTypes', () => {
|
||||
it('should return supported types array', () => {
|
||||
const types = middleware.getSupportedTypes();
|
||||
expect(Array.isArray(types)).toBe(true);
|
||||
expect(types.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('addSupportedType', () => {
|
||||
it('should add new supported type', () => {
|
||||
middleware.addSupportedType('application/xml');
|
||||
const types = middleware.getSupportedTypes();
|
||||
expect(types).toContain('application/xml');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -6,14 +6,29 @@
|
||||
* - 确保API接口接收正确的数据格式
|
||||
* - 提供友好的错误提示信息
|
||||
*
|
||||
* 职责分离:
|
||||
* - Content-Type验证逻辑的实现
|
||||
* - 支持类型和排除路径的配置管理
|
||||
* - 错误响应的统一格式化处理
|
||||
*
|
||||
* 主要方法:
|
||||
* - use() - 中间件处理入口方法
|
||||
* - shouldCheckContentType() - 检查条件判断逻辑
|
||||
* - isSupportedContentType() - 类型支持性验证
|
||||
* - normalizeContentType() - 类型标准化处理
|
||||
*
|
||||
* 使用场景:
|
||||
* - API接口数据格式验证
|
||||
* - 防止错误的请求格式
|
||||
* - 提升API接口的健壮性
|
||||
*
|
||||
* @author kiro-ai
|
||||
* @version 1.0.0
|
||||
* 最近修改:
|
||||
* - 2026-01-07: 代码规范优化 - 更新注释规范,完善中间件说明
|
||||
*
|
||||
* @author moyin
|
||||
* @version 1.0.1
|
||||
* @since 2025-12-24
|
||||
* @lastModified 2026-01-07
|
||||
*/
|
||||
|
||||
import { Injectable, NestMiddleware } from '@nestjs/common';
|
||||
@@ -1,27 +0,0 @@
|
||||
/**
|
||||
* 核心安全模块导出
|
||||
*
|
||||
* 功能概述:
|
||||
* - 频率限制和防护机制
|
||||
* - 请求超时控制
|
||||
* - 维护模式管理
|
||||
* - 内容类型验证
|
||||
* - 系统安全中间件
|
||||
*/
|
||||
|
||||
// 模块
|
||||
export * from './security_core.module';
|
||||
|
||||
// 守卫
|
||||
export * from './guards/throttle.guard';
|
||||
|
||||
// 中间件
|
||||
export * from './middleware/maintenance.middleware';
|
||||
export * from './middleware/content_type.middleware';
|
||||
|
||||
// 拦截器
|
||||
export * from './interceptors/timeout.interceptor';
|
||||
|
||||
// 装饰器
|
||||
export * from './decorators/throttle.decorator';
|
||||
export * from './decorators/timeout.decorator';
|
||||
132
src/core/security_core/maintenance.middleware.spec.ts
Normal file
132
src/core/security_core/maintenance.middleware.spec.ts
Normal file
@@ -0,0 +1,132 @@
|
||||
/**
|
||||
* MaintenanceMiddleware 单元测试
|
||||
*
|
||||
* @author moyin
|
||||
* @version 1.0.0
|
||||
* @since 2026-01-07
|
||||
*/
|
||||
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { Request, Response, NextFunction } from 'express';
|
||||
import { MaintenanceMiddleware } from './maintenance.middleware';
|
||||
|
||||
describe('MaintenanceMiddleware', () => {
|
||||
let middleware: MaintenanceMiddleware;
|
||||
let configService: jest.Mocked<ConfigService>;
|
||||
let mockRequest: Partial<Request>;
|
||||
let mockResponse: Partial<Response>;
|
||||
let mockNext: NextFunction;
|
||||
|
||||
beforeEach(async () => {
|
||||
const mockConfigService = {
|
||||
get: jest.fn(),
|
||||
};
|
||||
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
MaintenanceMiddleware,
|
||||
{ provide: ConfigService, useValue: mockConfigService },
|
||||
],
|
||||
}).compile();
|
||||
|
||||
middleware = module.get<MaintenanceMiddleware>(MaintenanceMiddleware);
|
||||
configService = module.get(ConfigService);
|
||||
|
||||
mockRequest = {
|
||||
method: 'GET',
|
||||
url: '/api/test',
|
||||
get: jest.fn(),
|
||||
ip: '127.0.0.1',
|
||||
};
|
||||
|
||||
mockResponse = {
|
||||
status: jest.fn().mockReturnThis(),
|
||||
json: jest.fn().mockReturnThis(),
|
||||
setHeader: jest.fn().mockReturnThis(),
|
||||
};
|
||||
|
||||
mockNext = jest.fn();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('use', () => {
|
||||
it('should call next() when maintenance mode is disabled', () => {
|
||||
// Arrange
|
||||
configService.get.mockReturnValue('false');
|
||||
|
||||
// Act
|
||||
middleware.use(mockRequest as Request, mockResponse as Response, mockNext);
|
||||
|
||||
// Assert
|
||||
expect(mockNext).toHaveBeenCalled();
|
||||
expect(mockResponse.status).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should return 503 when maintenance mode is enabled', () => {
|
||||
// Arrange
|
||||
configService.get.mockImplementation((key) => {
|
||||
switch (key) {
|
||||
case 'MAINTENANCE_MODE': return 'true';
|
||||
case 'MAINTENANCE_RETRY_AFTER': return 3600;
|
||||
default: return undefined;
|
||||
}
|
||||
});
|
||||
|
||||
// Act
|
||||
middleware.use(mockRequest as Request, mockResponse as Response, mockNext);
|
||||
|
||||
// Assert
|
||||
expect(mockResponse.status).toHaveBeenCalledWith(503);
|
||||
expect(mockNext).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('isMaintenanceEnabled', () => {
|
||||
it('should return true when maintenance mode is enabled', () => {
|
||||
// Arrange
|
||||
configService.get.mockReturnValue('true');
|
||||
|
||||
// Act
|
||||
const result = middleware.isMaintenanceEnabled();
|
||||
|
||||
// Assert
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false when maintenance mode is disabled', () => {
|
||||
// Arrange
|
||||
configService.get.mockReturnValue('false');
|
||||
|
||||
// Act
|
||||
const result = middleware.isMaintenanceEnabled();
|
||||
|
||||
// Assert
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getMaintenanceInfo', () => {
|
||||
it('should return maintenance info', () => {
|
||||
// Arrange
|
||||
configService.get.mockImplementation((key) => {
|
||||
switch (key) {
|
||||
case 'MAINTENANCE_MODE': return 'true';
|
||||
case 'MAINTENANCE_START_TIME': return '2026-01-07T10:00:00.000Z';
|
||||
default: return undefined;
|
||||
}
|
||||
});
|
||||
|
||||
// Act
|
||||
const info = middleware.getMaintenanceInfo();
|
||||
|
||||
// Assert
|
||||
expect(info).toBeDefined();
|
||||
expect(info.enabled).toBe(true);
|
||||
expect(info.startTime).toBe('2026-01-07T10:00:00.000Z');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -6,15 +6,29 @@
|
||||
* - 在维护期间阻止用户访问API
|
||||
* - 提供维护状态和预计恢复时间信息
|
||||
*
|
||||
* 职责分离:
|
||||
* - 维护模式状态检查逻辑
|
||||
* - 维护配置信息的读取和管理
|
||||
* - 维护响应的统一格式化处理
|
||||
*
|
||||
* 主要方法:
|
||||
* - use() - 中间件处理入口方法
|
||||
* - isMaintenanceEnabled() - 维护模式状态检查
|
||||
* - getMaintenanceInfo() - 维护信息获取
|
||||
*
|
||||
* 使用场景:
|
||||
* - 系统升级维护
|
||||
* - 数据库迁移
|
||||
* - 紧急故障修复
|
||||
* - 定期维护窗口
|
||||
*
|
||||
* @author kiro-ai
|
||||
* @version 1.0.0
|
||||
* 最近修改:
|
||||
* - 2026-01-07: 代码规范优化 - 更新注释规范,完善中间件说明
|
||||
*
|
||||
* @author moyin
|
||||
* @version 1.0.1
|
||||
* @since 2025-12-24
|
||||
* @lastModified 2026-01-07
|
||||
*/
|
||||
|
||||
import { Injectable, NestMiddleware } from '@nestjs/common';
|
||||
62
src/core/security_core/security_core.module.spec.ts
Normal file
62
src/core/security_core/security_core.module.spec.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
/**
|
||||
* SecurityCoreModule 单元测试
|
||||
*
|
||||
* 测试覆盖:
|
||||
* - 模块配置验证
|
||||
* - 提供者注册检查
|
||||
* - 导出验证
|
||||
*
|
||||
* @author moyin
|
||||
* @version 1.0.0
|
||||
* @since 2026-01-07
|
||||
*/
|
||||
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { APP_GUARD, APP_INTERCEPTOR } from '@nestjs/core';
|
||||
import { SecurityCoreModule } from './security_core.module';
|
||||
import { ThrottleGuard } from './throttle.guard';
|
||||
import { TimeoutInterceptor } from './timeout.interceptor';
|
||||
|
||||
describe('SecurityCoreModule', () => {
|
||||
let module: TestingModule;
|
||||
|
||||
beforeEach(async () => {
|
||||
module = await Test.createTestingModule({
|
||||
imports: [SecurityCoreModule],
|
||||
}).compile();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await module.close();
|
||||
});
|
||||
|
||||
describe('Module Configuration', () => {
|
||||
it('should be defined', () => {
|
||||
expect(module).toBeDefined();
|
||||
});
|
||||
|
||||
it('should provide ThrottleGuard', () => {
|
||||
const guard = module.get<ThrottleGuard>(ThrottleGuard);
|
||||
expect(guard).toBeDefined();
|
||||
expect(guard).toBeInstanceOf(ThrottleGuard);
|
||||
});
|
||||
|
||||
it('should provide TimeoutInterceptor', () => {
|
||||
const interceptor = module.get<TimeoutInterceptor>(TimeoutInterceptor);
|
||||
expect(interceptor).toBeDefined();
|
||||
expect(interceptor).toBeInstanceOf(TimeoutInterceptor);
|
||||
});
|
||||
|
||||
it('should provide global providers', () => {
|
||||
// 验证模块能够正常编译和初始化
|
||||
expect(module).toBeDefined();
|
||||
|
||||
// 验证核心组件可以被获取
|
||||
const guard = module.get<ThrottleGuard>(ThrottleGuard);
|
||||
const interceptor = module.get<TimeoutInterceptor>(TimeoutInterceptor);
|
||||
|
||||
expect(guard).toBeDefined();
|
||||
expect(interceptor).toBeDefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -7,15 +7,24 @@
|
||||
* - 维护模式和内容类型验证
|
||||
* - 全局安全中间件和守卫
|
||||
*
|
||||
* @author kiro-ai
|
||||
* @version 1.0.0
|
||||
* 职责分离:
|
||||
* - 安全组件注册和配置管理
|
||||
* - 全局守卫和拦截器的依赖注入
|
||||
* - 安全功能的统一导出和模块化
|
||||
*
|
||||
* 最近修改:
|
||||
* - 2026-01-07: 代码规范优化 - 更新注释规范,完善文档说明
|
||||
*
|
||||
* @author moyin
|
||||
* @version 1.0.1
|
||||
* @since 2025-12-24
|
||||
* @lastModified 2026-01-07
|
||||
*/
|
||||
|
||||
import { Module } from '@nestjs/common';
|
||||
import { APP_GUARD, APP_INTERCEPTOR } from '@nestjs/core';
|
||||
import { ThrottleGuard } from './guards/throttle.guard';
|
||||
import { TimeoutInterceptor } from './interceptors/timeout.interceptor';
|
||||
import { ThrottleGuard } from './throttle.guard';
|
||||
import { TimeoutInterceptor } from './timeout.interceptor';
|
||||
|
||||
@Module({
|
||||
providers: [
|
||||
|
||||
@@ -6,19 +6,28 @@
|
||||
* - 防止恶意请求和系统滥用
|
||||
* - 支持基于IP和用户的限制策略
|
||||
*
|
||||
* 职责分离:
|
||||
* - 装饰器定义和配置接口管理
|
||||
* - 预设配置常量的维护
|
||||
* - 频率限制元数据的设置逻辑
|
||||
*
|
||||
* 使用场景:
|
||||
* - 登录接口防暴力破解
|
||||
* - 注册接口防批量注册
|
||||
* - 验证码接口防频繁发送
|
||||
* - 敏感操作接口保护
|
||||
*
|
||||
* @author kiro-ai
|
||||
* @version 1.0.0
|
||||
* 最近修改:
|
||||
* - 2026-01-07: 代码规范优化 - 更新注释规范,完善装饰器说明
|
||||
*
|
||||
* @author moyin
|
||||
* @version 1.0.1
|
||||
* @since 2025-12-24
|
||||
* @lastModified 2026-01-07
|
||||
*/
|
||||
|
||||
import { SetMetadata, applyDecorators, UseGuards } from '@nestjs/common';
|
||||
import { ThrottleGuard } from '../guards/throttle.guard';
|
||||
import { ThrottleGuard } from './throttle.guard';
|
||||
|
||||
/**
|
||||
* 频率限制元数据键
|
||||
@@ -42,8 +51,15 @@ export interface ThrottleConfig {
|
||||
/**
|
||||
* 频率限制装饰器
|
||||
*
|
||||
* 业务逻辑:
|
||||
* 1. 接收频率限制配置参数
|
||||
* 2. 设置频率限制元数据到方法或类上
|
||||
* 3. 应用ThrottleGuard守卫进行实际限制检查
|
||||
* 4. 支持自定义错误消息和限制类型
|
||||
*
|
||||
* @param config 频率限制配置
|
||||
* @returns 装饰器函数
|
||||
* @throws HttpException 当请求频率超过限制时
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
118
src/core/security_core/throttle.guard.spec.ts
Normal file
118
src/core/security_core/throttle.guard.spec.ts
Normal file
@@ -0,0 +1,118 @@
|
||||
/**
|
||||
* ThrottleGuard 单元测试
|
||||
*
|
||||
* @author moyin
|
||||
* @version 1.0.0
|
||||
* @since 2026-01-07
|
||||
*/
|
||||
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { ExecutionContext, HttpException, HttpStatus } from '@nestjs/common';
|
||||
import { Reflector } from '@nestjs/core';
|
||||
import { ThrottleGuard } from './throttle.guard';
|
||||
import { ThrottleConfig } from './throttle.decorator';
|
||||
|
||||
describe('ThrottleGuard', () => {
|
||||
let guard: ThrottleGuard;
|
||||
let reflector: jest.Mocked<Reflector>;
|
||||
|
||||
beforeEach(async () => {
|
||||
const mockReflector = {
|
||||
get: jest.fn(),
|
||||
};
|
||||
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
ThrottleGuard,
|
||||
{ provide: Reflector, useValue: mockReflector },
|
||||
],
|
||||
}).compile();
|
||||
|
||||
guard = module.get<ThrottleGuard>(ThrottleGuard);
|
||||
reflector = module.get(Reflector);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
guard.clearAllRecords();
|
||||
});
|
||||
|
||||
describe('canActivate', () => {
|
||||
it('should allow request when no throttle config is found', async () => {
|
||||
// Arrange
|
||||
reflector.get.mockReturnValue(null);
|
||||
const mockContext = createMockContext();
|
||||
|
||||
// Act
|
||||
const result = await guard.canActivate(mockContext);
|
||||
|
||||
// Assert
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should allow first request within limit', async () => {
|
||||
// Arrange
|
||||
const config: ThrottleConfig = { limit: 5, ttl: 60 };
|
||||
reflector.get.mockReturnValueOnce(config).mockReturnValueOnce(null);
|
||||
const mockContext = createMockContext();
|
||||
|
||||
// Act
|
||||
const result = await guard.canActivate(mockContext);
|
||||
|
||||
// Assert
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('should throw HttpException when limit exceeded', async () => {
|
||||
// Arrange
|
||||
const config: ThrottleConfig = { limit: 1, ttl: 60 };
|
||||
reflector.get.mockReturnValue(config);
|
||||
const mockContext = createMockContext();
|
||||
|
||||
// Act - first request should pass
|
||||
await guard.canActivate(mockContext);
|
||||
|
||||
// Assert - second request should throw
|
||||
await expect(guard.canActivate(mockContext)).rejects.toThrow(HttpException);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getStats', () => {
|
||||
it('should return empty stats initially', () => {
|
||||
const stats = guard.getStats();
|
||||
expect(stats.totalRecords).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('clearAllRecords', () => {
|
||||
it('should clear all records', () => {
|
||||
guard.clearAllRecords();
|
||||
const stats = guard.getStats();
|
||||
expect(stats.totalRecords).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('onModuleDestroy', () => {
|
||||
it('should cleanup resources', () => {
|
||||
expect(() => guard.onModuleDestroy()).not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
function createMockContext(): ExecutionContext {
|
||||
const mockRequest = {
|
||||
ip: '127.0.0.1',
|
||||
method: 'POST',
|
||||
url: '/api/test',
|
||||
route: { path: '/api/test' },
|
||||
get: jest.fn(),
|
||||
};
|
||||
|
||||
return {
|
||||
switchToHttp: jest.fn().mockReturnValue({
|
||||
getRequest: jest.fn().mockReturnValue(mockRequest),
|
||||
}),
|
||||
getHandler: jest.fn(),
|
||||
getClass: jest.fn(),
|
||||
} as any;
|
||||
}
|
||||
});
|
||||
@@ -6,14 +6,29 @@
|
||||
* - 基于IP地址进行限制
|
||||
* - 支持自定义限制规则
|
||||
*
|
||||
* 职责分离:
|
||||
* - 频率限制逻辑的核心实现
|
||||
* - 请求记录的内存存储和管理
|
||||
* - 限制检查和异常处理
|
||||
*
|
||||
* 主要方法:
|
||||
* - canActivate() - 守卫检查入口方法
|
||||
* - checkThrottle() - 频率限制核心检查逻辑
|
||||
* - generateKey() - 限制键生成算法
|
||||
* - cleanupExpiredRecords() - 过期记录清理机制
|
||||
*
|
||||
* 使用场景:
|
||||
* - 防止API滥用
|
||||
* - 登录暴力破解防护
|
||||
* - 验证码发送频率控制
|
||||
*
|
||||
* @author kiro-ai
|
||||
* @version 1.0.0
|
||||
* 最近修改:
|
||||
* - 2026-01-07: 代码规范优化 - 更新注释规范,完善守卫说明
|
||||
*
|
||||
* @author moyin
|
||||
* @version 1.0.1
|
||||
* @since 2025-12-24
|
||||
* @lastModified 2026-01-07
|
||||
*/
|
||||
|
||||
import {
|
||||
@@ -22,11 +37,12 @@ import {
|
||||
ExecutionContext,
|
||||
HttpException,
|
||||
HttpStatus,
|
||||
Logger
|
||||
Logger,
|
||||
OnModuleDestroy
|
||||
} from '@nestjs/common';
|
||||
import { Reflector } from '@nestjs/core';
|
||||
import { Request } from 'express';
|
||||
import { THROTTLE_KEY, ThrottleConfig } from '../decorators/throttle.decorator';
|
||||
import { THROTTLE_KEY, ThrottleConfig } from './throttle.decorator';
|
||||
|
||||
/**
|
||||
* 频率限制记录接口
|
||||
@@ -64,7 +80,7 @@ interface ThrottleResponse {
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class ThrottleGuard implements CanActivate {
|
||||
export class ThrottleGuard implements CanActivate, OnModuleDestroy {
|
||||
private readonly logger = new Logger(ThrottleGuard.name);
|
||||
|
||||
/**
|
||||
@@ -77,18 +93,48 @@ export class ThrottleGuard implements CanActivate {
|
||||
/**
|
||||
* 清理过期记录的间隔(毫秒)
|
||||
*/
|
||||
private readonly cleanupInterval = 60000; // 1分钟
|
||||
private readonly CLEANUP_INTERVAL = 60000; // 1分钟
|
||||
|
||||
/**
|
||||
* 清理任务的定时器ID
|
||||
*/
|
||||
private cleanupTimer?: NodeJS.Timeout;
|
||||
|
||||
constructor(private readonly reflector: Reflector) {
|
||||
// 启动定期清理任务
|
||||
this.startCleanupTask();
|
||||
}
|
||||
|
||||
/**
|
||||
* 组件销毁时的清理方法
|
||||
*/
|
||||
onModuleDestroy() {
|
||||
if (this.cleanupTimer) {
|
||||
clearInterval(this.cleanupTimer);
|
||||
this.cleanupTimer = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 守卫检查函数
|
||||
*
|
||||
* 业务逻辑:
|
||||
* 1. 从装饰器元数据获取频率限制配置
|
||||
* 2. 提取请求信息(IP、路径、方法等)
|
||||
* 3. 生成唯一的限制键标识
|
||||
* 4. 检查当前请求是否超过频率限制
|
||||
* 5. 记录被限制的请求日志
|
||||
* 6. 抛出频率限制异常或允许请求通过
|
||||
*
|
||||
* @param context 执行上下文
|
||||
* @returns 是否允许通过
|
||||
* @throws HttpException 当请求频率超过限制时抛出429状态码
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // 守卫会自动应用到使用@Throttle装饰器的方法上
|
||||
* // 无需手动调用此方法
|
||||
* ```
|
||||
*/
|
||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||
// 1. 获取频率限制配置
|
||||
@@ -263,9 +309,9 @@ export class ThrottleGuard implements CanActivate {
|
||||
* 启动清理任务
|
||||
*/
|
||||
private startCleanupTask(): void {
|
||||
setInterval(() => {
|
||||
this.cleanupTimer = setInterval(() => {
|
||||
this.cleanupExpiredRecords();
|
||||
}, this.cleanupInterval);
|
||||
}, this.CLEANUP_INTERVAL);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -273,10 +319,10 @@ export class ThrottleGuard implements CanActivate {
|
||||
*/
|
||||
private cleanupExpiredRecords(): void {
|
||||
const now = Date.now();
|
||||
const maxAge = 3600000; // 1小时
|
||||
const MAX_AGE = 3600000; // 1小时
|
||||
|
||||
for (const [key, record] of this.records.entries()) {
|
||||
if (now - record.lastRequest > maxAge) {
|
||||
if (now - record.lastRequest > MAX_AGE) {
|
||||
this.records.delete(key);
|
||||
}
|
||||
}
|
||||
@@ -6,15 +6,24 @@
|
||||
* - 防止长时间运行的请求阻塞系统
|
||||
* - 提供友好的超时错误提示
|
||||
*
|
||||
* 职责分离:
|
||||
* - 超时装饰器定义和配置管理
|
||||
* - 预设超时配置常量的维护
|
||||
* - 超时元数据的设置和Swagger文档生成
|
||||
*
|
||||
* 使用场景:
|
||||
* - 数据库查询超时控制
|
||||
* - 外部API调用超时
|
||||
* - 文件上传下载超时
|
||||
* - 复杂计算任务超时
|
||||
*
|
||||
* @author kiro-ai
|
||||
* @version 1.0.0
|
||||
* 最近修改:
|
||||
* - 2026-01-07: 代码规范优化 - 更新注释规范,完善装饰器说明
|
||||
*
|
||||
* @author moyin
|
||||
* @version 1.0.1
|
||||
* @since 2025-12-24
|
||||
* @lastModified 2026-01-07
|
||||
*/
|
||||
|
||||
import { SetMetadata, applyDecorators } from '@nestjs/common';
|
||||
@@ -40,8 +49,15 @@ export interface TimeoutConfig {
|
||||
/**
|
||||
* 超时装饰器
|
||||
*
|
||||
* 业务逻辑:
|
||||
* 1. 接收超时配置参数(数字或配置对象)
|
||||
* 2. 标准化超时配置格式
|
||||
* 3. 设置超时元数据到方法或类上
|
||||
* 4. 生成对应的Swagger API响应文档
|
||||
*
|
||||
* @param config 超时配置或超时时间(毫秒)
|
||||
* @returns 装饰器函数
|
||||
* @throws RequestTimeoutException 当请求执行时间超过设定值时
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
101
src/core/security_core/timeout.interceptor.spec.ts
Normal file
101
src/core/security_core/timeout.interceptor.spec.ts
Normal file
@@ -0,0 +1,101 @@
|
||||
/**
|
||||
* TimeoutInterceptor 单元测试
|
||||
*
|
||||
* @author moyin
|
||||
* @version 1.0.0
|
||||
* @since 2026-01-07
|
||||
*/
|
||||
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import { ExecutionContext, CallHandler } from '@nestjs/common';
|
||||
import { Reflector } from '@nestjs/core';
|
||||
import { of } from 'rxjs';
|
||||
import { TimeoutInterceptor } from './timeout.interceptor';
|
||||
import { TimeoutConfig } from './timeout.decorator';
|
||||
|
||||
describe('TimeoutInterceptor', () => {
|
||||
let interceptor: TimeoutInterceptor;
|
||||
let reflector: jest.Mocked<Reflector>;
|
||||
|
||||
beforeEach(async () => {
|
||||
const mockReflector = {
|
||||
get: jest.fn(),
|
||||
};
|
||||
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
TimeoutInterceptor,
|
||||
{ provide: Reflector, useValue: mockReflector },
|
||||
],
|
||||
}).compile();
|
||||
|
||||
interceptor = module.get<TimeoutInterceptor>(TimeoutInterceptor);
|
||||
reflector = module.get(Reflector);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('intercept', () => {
|
||||
it('should pass through when no timeout config is found', (done) => {
|
||||
// Arrange
|
||||
reflector.get.mockReturnValue(null);
|
||||
const testData = { result: 'success' };
|
||||
const mockCallHandler: CallHandler = {
|
||||
handle: jest.fn().mockReturnValue(of(testData)),
|
||||
};
|
||||
const mockContext = createMockContext();
|
||||
|
||||
// Act
|
||||
const result$ = interceptor.intercept(mockContext, mockCallHandler);
|
||||
|
||||
// Assert
|
||||
result$.subscribe({
|
||||
next: (data) => {
|
||||
expect(data).toEqual(testData);
|
||||
done();
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should apply timeout when config is found', (done) => {
|
||||
// Arrange
|
||||
const config: TimeoutConfig = { timeout: 1000 };
|
||||
reflector.get.mockReturnValueOnce(config).mockReturnValueOnce(null);
|
||||
const testData = { result: 'success' };
|
||||
const mockCallHandler: CallHandler = {
|
||||
handle: jest.fn().mockReturnValue(of(testData)),
|
||||
};
|
||||
const mockContext = createMockContext();
|
||||
|
||||
// Act
|
||||
const result$ = interceptor.intercept(mockContext, mockCallHandler);
|
||||
|
||||
// Assert
|
||||
result$.subscribe({
|
||||
next: (data) => {
|
||||
expect(data).toEqual(testData);
|
||||
done();
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function createMockContext(): ExecutionContext {
|
||||
const mockRequest = {
|
||||
method: 'GET',
|
||||
url: '/api/test',
|
||||
get: jest.fn(),
|
||||
ip: '127.0.0.1',
|
||||
};
|
||||
|
||||
return {
|
||||
switchToHttp: jest.fn().mockReturnValue({
|
||||
getRequest: jest.fn().mockReturnValue(mockRequest),
|
||||
}),
|
||||
getHandler: jest.fn(),
|
||||
getClass: jest.fn(),
|
||||
} as any;
|
||||
}
|
||||
});
|
||||
@@ -6,14 +6,29 @@
|
||||
* - 在超时时自动取消请求并返回错误
|
||||
* - 记录超时事件的详细日志
|
||||
*
|
||||
* 职责分离:
|
||||
* - 超时控制逻辑的核心实现
|
||||
* - 超时异常的统一处理和响应格式化
|
||||
* - 超时事件的日志记录和监控
|
||||
*
|
||||
* 主要方法:
|
||||
* - intercept() - 拦截器处理入口方法
|
||||
* - getTimeoutConfig() - 超时配置获取逻辑
|
||||
* - getDefaultTimeoutConfig() - 默认配置提供
|
||||
* - isValidTimeoutConfig() - 配置有效性验证
|
||||
*
|
||||
* 使用场景:
|
||||
* - 全局超时控制
|
||||
* - 防止资源泄漏
|
||||
* - 提升系统稳定性
|
||||
*
|
||||
* @author kiro-ai
|
||||
* @version 1.0.0
|
||||
* 最近修改:
|
||||
* - 2026-01-07: 代码规范优化 - 更新注释规范,完善拦截器说明
|
||||
*
|
||||
* @author moyin
|
||||
* @version 1.0.1
|
||||
* @since 2025-12-24
|
||||
* @lastModified 2026-01-07
|
||||
*/
|
||||
|
||||
import {
|
||||
@@ -27,7 +42,7 @@ import {
|
||||
import { Reflector } from '@nestjs/core';
|
||||
import { Observable, throwError, TimeoutError } from 'rxjs';
|
||||
import { catchError, timeout } from 'rxjs/operators';
|
||||
import { TIMEOUT_KEY, TimeoutConfig } from '../decorators/timeout.decorator';
|
||||
import { TIMEOUT_KEY, TimeoutConfig } from './timeout.decorator';
|
||||
|
||||
/**
|
||||
* 超时响应接口
|
||||
Reference in New Issue
Block a user