refactor:重构安全模块架构,将security模块迁移至core层
- 将src/business/security模块迁移至src/core/security_core - 更新模块导入路径和依赖关系 - 统一安全相关组件的命名规范(content_type.middleware.ts) - 清理过时的配置文件和文档 - 更新架构文档以反映新的模块结构 此次重构符合业务功能模块化架构设计原则,将技术基础设施 服务统一放置在core层,提高代码组织的清晰度和可维护性。
This commit is contained in:
@@ -25,7 +25,7 @@ import {
|
||||
AdminUserResponseDto,
|
||||
AdminRuntimeLogsResponseDto
|
||||
} from './dto/admin-response.dto';
|
||||
import { Throttle, ThrottlePresets } from '../security/decorators/throttle.decorator';
|
||||
import { Throttle, ThrottlePresets } from '../../core/security_core/decorators/throttle.decorator';
|
||||
import type { Response } from 'express';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
@@ -33,8 +33,8 @@ import {
|
||||
TestModeEmailVerificationResponseDto,
|
||||
SuccessEmailVerificationResponseDto
|
||||
} from '../dto/login_response.dto';
|
||||
import { Throttle, ThrottlePresets } from '../../security/decorators/throttle.decorator';
|
||||
import { Timeout, TimeoutPresets } from '../../security/decorators/timeout.decorator';
|
||||
import { Throttle, ThrottlePresets } from '../../../core/security_core/decorators/throttle.decorator';
|
||||
import { Timeout, TimeoutPresets } from '../../../core/security_core/decorators/timeout.decorator';
|
||||
|
||||
@ApiTags('auth')
|
||||
@Controller('auth')
|
||||
|
||||
@@ -1,89 +0,0 @@
|
||||
/**
|
||||
* 频率限制装饰器
|
||||
*
|
||||
* 功能描述:
|
||||
* - 提供API接口的频率限制功能
|
||||
* - 防止恶意请求和系统滥用
|
||||
* - 支持基于IP和用户的限制策略
|
||||
*
|
||||
* 使用场景:
|
||||
* - 登录接口防暴力破解
|
||||
* - 注册接口防批量注册
|
||||
* - 验证码接口防频繁发送
|
||||
* - 敏感操作接口保护
|
||||
*
|
||||
* @author kiro-ai
|
||||
* @version 1.0.0
|
||||
* @since 2025-12-24
|
||||
*/
|
||||
|
||||
import { SetMetadata, applyDecorators, UseGuards } from '@nestjs/common';
|
||||
import { ThrottleGuard } from '../guards/throttle.guard';
|
||||
|
||||
/**
|
||||
* 频率限制元数据键
|
||||
*/
|
||||
export const THROTTLE_KEY = 'throttle';
|
||||
|
||||
/**
|
||||
* 频率限制配置接口
|
||||
*/
|
||||
export interface ThrottleConfig {
|
||||
/** 时间窗口内允许的最大请求次数 */
|
||||
limit: number;
|
||||
/** 时间窗口长度(秒) */
|
||||
ttl: number;
|
||||
/** 限制类型:ip(基于IP)或 user(基于用户) */
|
||||
type?: 'ip' | 'user';
|
||||
/** 自定义错误消息 */
|
||||
message?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 频率限制装饰器
|
||||
*
|
||||
* @param config 频率限制配置
|
||||
* @returns 装饰器函数
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // 每分钟最多5次登录尝试
|
||||
* @Throttle({ limit: 5, ttl: 60, message: '登录尝试过于频繁,请稍后再试' })
|
||||
* @Post('login')
|
||||
* async login() { ... }
|
||||
*
|
||||
* // 每5分钟最多3次注册
|
||||
* @Throttle({ limit: 3, ttl: 300, type: 'ip' })
|
||||
* @Post('register')
|
||||
* async register() { ... }
|
||||
* ```
|
||||
*/
|
||||
export function Throttle(config: ThrottleConfig) {
|
||||
return applyDecorators(
|
||||
SetMetadata(THROTTLE_KEY, config),
|
||||
UseGuards(ThrottleGuard)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 预定义的频率限制配置
|
||||
*/
|
||||
export const ThrottlePresets = {
|
||||
/** 登录接口:每分钟5次 */
|
||||
LOGIN: { limit: 5, ttl: 60, message: '登录尝试过于频繁,请1分钟后再试' },
|
||||
|
||||
/** 注册接口:每5分钟10次(开发环境放宽限制) */
|
||||
REGISTER: { limit: 10, ttl: 300, message: '注册请求过于频繁,请5分钟后再试' },
|
||||
|
||||
/** 发送验证码:每分钟1次 */
|
||||
SEND_CODE: { limit: 1, ttl: 60, message: '验证码发送过于频繁,请1分钟后再试' },
|
||||
|
||||
/** 密码重置:每小时3次 */
|
||||
RESET_PASSWORD: { limit: 3, ttl: 3600, message: '密码重置请求过于频繁,请1小时后再试' },
|
||||
|
||||
/** 管理员操作:每分钟10次 */
|
||||
ADMIN_OPERATION: { limit: 10, ttl: 60, message: '管理员操作过于频繁,请稍后再试' },
|
||||
|
||||
/** 一般API:每分钟30次 */
|
||||
GENERAL_API: { limit: 30, ttl: 60, message: 'API调用过于频繁,请稍后再试' }
|
||||
} as const;
|
||||
@@ -1,119 +0,0 @@
|
||||
/**
|
||||
* 超时处理装饰器
|
||||
*
|
||||
* 功能描述:
|
||||
* - 为API接口添加超时控制
|
||||
* - 防止长时间运行的请求阻塞系统
|
||||
* - 提供友好的超时错误提示
|
||||
*
|
||||
* 使用场景:
|
||||
* - 数据库查询超时控制
|
||||
* - 外部API调用超时
|
||||
* - 文件上传下载超时
|
||||
* - 复杂计算任务超时
|
||||
*
|
||||
* @author kiro-ai
|
||||
* @version 1.0.0
|
||||
* @since 2025-12-24
|
||||
*/
|
||||
|
||||
import { SetMetadata, applyDecorators } from '@nestjs/common';
|
||||
import { ApiResponse } from '@nestjs/swagger';
|
||||
|
||||
/**
|
||||
* 超时配置元数据键
|
||||
*/
|
||||
export const TIMEOUT_KEY = 'timeout';
|
||||
|
||||
/**
|
||||
* 超时配置接口
|
||||
*/
|
||||
export interface TimeoutConfig {
|
||||
/** 超时时间(毫秒) */
|
||||
timeout: number;
|
||||
/** 自定义超时错误消息 */
|
||||
message?: string;
|
||||
/** 是否记录超时日志 */
|
||||
logTimeout?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 超时装饰器
|
||||
*
|
||||
* @param config 超时配置或超时时间(毫秒)
|
||||
* @returns 装饰器函数
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // 设置30秒超时
|
||||
* @Timeout(30000)
|
||||
* @Get('slow-operation')
|
||||
* async slowOperation() { ... }
|
||||
*
|
||||
* // 自定义超时配置
|
||||
* @Timeout({
|
||||
* timeout: 60000,
|
||||
* message: '数据查询超时,请稍后重试',
|
||||
* logTimeout: true
|
||||
* })
|
||||
* @Post('complex-query')
|
||||
* async complexQuery() { ... }
|
||||
* ```
|
||||
*/
|
||||
export function Timeout(config: number | TimeoutConfig) {
|
||||
const timeoutConfig: TimeoutConfig = typeof config === 'number'
|
||||
? { timeout: config }
|
||||
: config;
|
||||
|
||||
return applyDecorators(
|
||||
SetMetadata(TIMEOUT_KEY, timeoutConfig),
|
||||
ApiResponse({
|
||||
status: 408,
|
||||
description: timeoutConfig.message || '请求超时',
|
||||
schema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
success: { type: 'boolean', example: false },
|
||||
message: { type: 'string', example: timeoutConfig.message || '请求超时,请稍后重试' },
|
||||
error_code: { type: 'string', example: 'REQUEST_TIMEOUT' },
|
||||
timeout_info: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
timeout_ms: { type: 'number', example: timeoutConfig.timeout },
|
||||
timestamp: { type: 'string', example: '2025-12-24T10:00:00.000Z' }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 预定义的超时配置
|
||||
*/
|
||||
export const TimeoutPresets = {
|
||||
/** 快速操作:5秒 */
|
||||
FAST: { timeout: 5000, message: '操作超时,请检查网络连接' },
|
||||
|
||||
/** 一般操作:30秒 */
|
||||
NORMAL: { timeout: 30000, message: '请求超时,请稍后重试' },
|
||||
|
||||
/** 慢操作:60秒 */
|
||||
SLOW: { timeout: 60000, message: '操作超时,请稍后重试' },
|
||||
|
||||
/** 文件操作:2分钟 */
|
||||
FILE_OPERATION: { timeout: 120000, message: '文件操作超时,请检查文件大小和网络状况' },
|
||||
|
||||
/** 数据库查询:45秒 */
|
||||
DATABASE_QUERY: { timeout: 45000, message: '数据查询超时,请简化查询条件或稍后重试' },
|
||||
|
||||
/** 外部API调用:15秒 */
|
||||
EXTERNAL_API: { timeout: 15000, message: '外部服务调用超时,请稍后重试' },
|
||||
|
||||
/** 邮件发送:30秒 */
|
||||
EMAIL_SEND: { timeout: 30000, message: '邮件发送超时,请检查邮件服务配置' },
|
||||
|
||||
/** 长时间任务:5分钟 */
|
||||
LONG_TASK: { timeout: 300000, message: '任务执行超时,请稍后重试' }
|
||||
} as const;
|
||||
@@ -1,317 +0,0 @@
|
||||
/**
|
||||
* 频率限制守卫
|
||||
*
|
||||
* 功能描述:
|
||||
* - 实现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);
|
||||
}
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
/**
|
||||
* 安全功能模块导出
|
||||
*
|
||||
* 功能概述:
|
||||
* - 频率限制和防护机制
|
||||
* - 请求超时控制
|
||||
* - 维护模式管理
|
||||
* - 内容类型验证
|
||||
* - 系统安全中间件
|
||||
*/
|
||||
|
||||
// 模块
|
||||
export * from './security.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';
|
||||
@@ -1,179 +0,0 @@
|
||||
/**
|
||||
* 超时拦截器
|
||||
*
|
||||
* 功能描述:
|
||||
* - 实现API接口的超时控制逻辑
|
||||
* - 在超时时自动取消请求并返回错误
|
||||
* - 记录超时事件的详细日志
|
||||
*
|
||||
* 使用场景:
|
||||
* - 全局超时控制
|
||||
* - 防止资源泄漏
|
||||
* - 提升系统稳定性
|
||||
*
|
||||
* @author kiro-ai
|
||||
* @version 1.0.0
|
||||
* @since 2025-12-24
|
||||
*/
|
||||
|
||||
import {
|
||||
Injectable,
|
||||
NestInterceptor,
|
||||
ExecutionContext,
|
||||
CallHandler,
|
||||
RequestTimeoutException,
|
||||
Logger
|
||||
} from '@nestjs/common';
|
||||
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';
|
||||
|
||||
/**
|
||||
* 超时响应接口
|
||||
*/
|
||||
interface TimeoutResponse {
|
||||
/** 请求是否成功 */
|
||||
success: boolean;
|
||||
/** 响应消息 */
|
||||
message: string;
|
||||
/** 错误代码 */
|
||||
error_code: string;
|
||||
/** 超时信息 */
|
||||
timeout_info: {
|
||||
/** 超时时间(毫秒) */
|
||||
timeout_ms: number;
|
||||
/** 超时发生时间 */
|
||||
timestamp: string;
|
||||
};
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class TimeoutInterceptor implements NestInterceptor {
|
||||
private readonly logger = new Logger(TimeoutInterceptor.name);
|
||||
|
||||
constructor(private readonly reflector: Reflector) {}
|
||||
|
||||
/**
|
||||
* 拦截器处理函数
|
||||
*
|
||||
* 业务逻辑:
|
||||
* 1. 获取超时配置
|
||||
* 2. 应用超时控制
|
||||
* 3. 处理超时异常
|
||||
* 4. 记录超时日志
|
||||
*
|
||||
* @param context 执行上下文
|
||||
* @param next 调用处理器
|
||||
* @returns 可观察对象
|
||||
*/
|
||||
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
|
||||
// 1. 获取超时配置
|
||||
const timeoutConfig = this.getTimeoutConfig(context);
|
||||
|
||||
if (!timeoutConfig) {
|
||||
// 没有配置超时,直接执行
|
||||
return next.handle();
|
||||
}
|
||||
|
||||
// 2. 获取请求信息用于日志记录
|
||||
const request = context.switchToHttp().getRequest();
|
||||
const startTime = Date.now();
|
||||
|
||||
// 3. 应用超时控制
|
||||
return next.handle().pipe(
|
||||
timeout(timeoutConfig.timeout),
|
||||
catchError((error) => {
|
||||
if (error instanceof TimeoutError) {
|
||||
// 4. 处理超时异常
|
||||
const duration = Date.now() - startTime;
|
||||
|
||||
// 5. 记录超时日志
|
||||
if (timeoutConfig.logTimeout !== false) {
|
||||
this.logger.warn('请求超时', {
|
||||
operation: 'request_timeout',
|
||||
method: request.method,
|
||||
url: request.url,
|
||||
timeout_ms: timeoutConfig.timeout,
|
||||
actual_duration_ms: duration,
|
||||
userAgent: request.get('User-Agent'),
|
||||
ip: request.ip,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
}
|
||||
|
||||
// 6. 构建超时响应
|
||||
const timeoutResponse: TimeoutResponse = {
|
||||
success: false,
|
||||
message: timeoutConfig.message || '请求超时,请稍后重试',
|
||||
error_code: 'REQUEST_TIMEOUT',
|
||||
timeout_info: {
|
||||
timeout_ms: timeoutConfig.timeout,
|
||||
timestamp: new Date().toISOString()
|
||||
}
|
||||
};
|
||||
|
||||
// 7. 抛出超时异常
|
||||
return throwError(() => new RequestTimeoutException(timeoutResponse));
|
||||
}
|
||||
|
||||
// 其他异常直接抛出
|
||||
return throwError(() => error);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取超时配置
|
||||
*
|
||||
* @param context 执行上下文
|
||||
* @returns 超时配置或null
|
||||
*/
|
||||
private getTimeoutConfig(context: ExecutionContext): TimeoutConfig | null {
|
||||
// 从方法装饰器获取配置
|
||||
const methodConfig = this.reflector.get<TimeoutConfig>(
|
||||
TIMEOUT_KEY,
|
||||
context.getHandler()
|
||||
);
|
||||
|
||||
if (methodConfig) {
|
||||
return methodConfig;
|
||||
}
|
||||
|
||||
// 从类装饰器获取配置
|
||||
const classConfig = this.reflector.get<TimeoutConfig>(
|
||||
TIMEOUT_KEY,
|
||||
context.getClass()
|
||||
);
|
||||
|
||||
return classConfig || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取默认超时配置
|
||||
*
|
||||
* @returns 默认超时配置
|
||||
*/
|
||||
private getDefaultTimeoutConfig(): TimeoutConfig {
|
||||
return {
|
||||
timeout: 30000, // 默认30秒
|
||||
message: '请求超时,请稍后重试',
|
||||
logTimeout: true
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证超时配置
|
||||
*
|
||||
* @param config 超时配置
|
||||
* @returns 是否有效
|
||||
*/
|
||||
private isValidTimeoutConfig(config: TimeoutConfig): boolean {
|
||||
return (
|
||||
config &&
|
||||
typeof config.timeout === 'number' &&
|
||||
config.timeout > 0 &&
|
||||
config.timeout <= 600000 // 最大10分钟
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,224 +0,0 @@
|
||||
/**
|
||||
* 内容类型检查中间件
|
||||
*
|
||||
* 功能描述:
|
||||
* - 检查POST/PUT请求的Content-Type头
|
||||
* - 确保API接口接收正确的数据格式
|
||||
* - 提供友好的错误提示信息
|
||||
*
|
||||
* 使用场景:
|
||||
* - API接口数据格式验证
|
||||
* - 防止错误的请求格式
|
||||
* - 提升API接口的健壮性
|
||||
*
|
||||
* @author kiro-ai
|
||||
* @version 1.0.0
|
||||
* @since 2025-12-24
|
||||
*/
|
||||
|
||||
import { Injectable, NestMiddleware } from '@nestjs/common';
|
||||
import { Request, Response, NextFunction } from 'express';
|
||||
import { Logger } from '@nestjs/common';
|
||||
|
||||
/**
|
||||
* 不支持的媒体类型响应接口
|
||||
*/
|
||||
interface UnsupportedMediaTypeResponse {
|
||||
/** 请求是否成功 */
|
||||
success: boolean;
|
||||
/** 响应消息 */
|
||||
message: string;
|
||||
/** 错误代码 */
|
||||
error_code: string;
|
||||
/** 支持的媒体类型 */
|
||||
supported_types: string[];
|
||||
/** 接收到的媒体类型 */
|
||||
received_type?: string;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class ContentTypeMiddleware implements NestMiddleware {
|
||||
private readonly logger = new Logger(ContentTypeMiddleware.name);
|
||||
|
||||
/**
|
||||
* 需要检查Content-Type的HTTP方法
|
||||
*/
|
||||
private readonly methodsToCheck = ['POST', 'PUT', 'PATCH'];
|
||||
|
||||
/**
|
||||
* 支持的Content-Type列表
|
||||
*/
|
||||
private readonly supportedTypes = [
|
||||
'application/json',
|
||||
'application/json; charset=utf-8'
|
||||
];
|
||||
|
||||
/**
|
||||
* 不需要检查Content-Type的路径(正则表达式)
|
||||
*/
|
||||
private readonly excludePaths = [
|
||||
/^\/api-docs/, // Swagger文档
|
||||
/^\/health/, // 健康检查
|
||||
/^\/admin\/logs\/archive/, // 文件下载
|
||||
/\/upload/, // 文件上传
|
||||
];
|
||||
|
||||
/**
|
||||
* 中间件处理函数
|
||||
*
|
||||
* 业务逻辑:
|
||||
* 1. 检查是否需要验证Content-Type
|
||||
* 2. 获取请求的Content-Type头
|
||||
* 3. 验证Content-Type是否支持
|
||||
* 4. 记录不支持的请求类型
|
||||
*
|
||||
* @param req HTTP请求对象
|
||||
* @param res HTTP响应对象
|
||||
* @param next 下一个中间件函数
|
||||
*/
|
||||
use(req: Request, res: Response, next: NextFunction) {
|
||||
// 1. 检查是否需要验证Content-Type
|
||||
if (!this.shouldCheckContentType(req)) {
|
||||
next();
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. 获取请求的Content-Type
|
||||
const contentType = req.get('Content-Type');
|
||||
|
||||
// 3. 检查Content-Type是否存在
|
||||
if (!contentType) {
|
||||
this.logger.warn('请求缺少Content-Type头', {
|
||||
operation: 'content_type_check',
|
||||
method: req.method,
|
||||
url: req.url,
|
||||
userAgent: req.get('User-Agent'),
|
||||
ip: req.ip,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
const response: UnsupportedMediaTypeResponse = {
|
||||
success: false,
|
||||
message: '请求缺少Content-Type头,请设置为application/json',
|
||||
error_code: 'MISSING_CONTENT_TYPE',
|
||||
supported_types: this.supportedTypes,
|
||||
received_type: undefined
|
||||
};
|
||||
|
||||
res.status(415).json(response);
|
||||
return;
|
||||
}
|
||||
|
||||
// 4. 验证Content-Type是否支持
|
||||
const normalizedContentType = this.normalizeContentType(contentType);
|
||||
|
||||
if (!this.isSupportedContentType(normalizedContentType)) {
|
||||
this.logger.warn('不支持的Content-Type', {
|
||||
operation: 'content_type_check',
|
||||
method: req.method,
|
||||
url: req.url,
|
||||
contentType: contentType,
|
||||
normalizedContentType: normalizedContentType,
|
||||
userAgent: req.get('User-Agent'),
|
||||
ip: req.ip,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
const response: UnsupportedMediaTypeResponse = {
|
||||
success: false,
|
||||
message: `不支持的Content-Type: ${contentType},请使用application/json`,
|
||||
error_code: 'UNSUPPORTED_MEDIA_TYPE',
|
||||
supported_types: this.supportedTypes,
|
||||
received_type: contentType
|
||||
};
|
||||
|
||||
res.status(415).json(response);
|
||||
return;
|
||||
}
|
||||
|
||||
// 5. Content-Type验证通过,继续处理
|
||||
next();
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否需要验证Content-Type
|
||||
*
|
||||
* @param req HTTP请求对象
|
||||
* @returns 是否需要验证
|
||||
*/
|
||||
private shouldCheckContentType(req: Request): boolean {
|
||||
// 1. 检查HTTP方法
|
||||
if (!this.methodsToCheck.includes(req.method)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 2. 检查是否在排除路径中
|
||||
const url = req.url;
|
||||
for (const excludePattern of this.excludePaths) {
|
||||
if (excludePattern.test(url)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 检查Content-Length,如果为0则不需要验证
|
||||
const contentLength = req.get('Content-Length');
|
||||
if (contentLength === '0') {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 标准化Content-Type
|
||||
*
|
||||
* @param contentType 原始Content-Type
|
||||
* @returns 标准化后的Content-Type
|
||||
*/
|
||||
private normalizeContentType(contentType: string): string {
|
||||
// 移除空格并转换为小写
|
||||
return contentType.toLowerCase().trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查Content-Type是否支持
|
||||
*
|
||||
* @param contentType 标准化的Content-Type
|
||||
* @returns 是否支持
|
||||
*/
|
||||
private isSupportedContentType(contentType: string): boolean {
|
||||
// 检查是否以支持的类型开头
|
||||
return this.supportedTypes.some(supportedType =>
|
||||
contentType.startsWith(supportedType.toLowerCase())
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取支持的Content-Type列表
|
||||
*
|
||||
* @returns 支持的类型列表
|
||||
*/
|
||||
getSupportedTypes(): string[] {
|
||||
return [...this.supportedTypes];
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加支持的Content-Type
|
||||
*
|
||||
* @param contentType 要添加的Content-Type
|
||||
*/
|
||||
addSupportedType(contentType: string): void {
|
||||
if (!this.supportedTypes.includes(contentType)) {
|
||||
this.supportedTypes.push(contentType);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加排除路径
|
||||
*
|
||||
* @param pattern 路径正则表达式
|
||||
*/
|
||||
addExcludePath(pattern: RegExp): void {
|
||||
this.excludePaths.push(pattern);
|
||||
}
|
||||
}
|
||||
@@ -1,137 +0,0 @@
|
||||
/**
|
||||
* 维护模式中间件
|
||||
*
|
||||
* 功能描述:
|
||||
* - 检查系统是否处于维护模式
|
||||
* - 在维护期间阻止用户访问API
|
||||
* - 提供维护状态和预计恢复时间信息
|
||||
*
|
||||
* 使用场景:
|
||||
* - 系统升级维护
|
||||
* - 数据库迁移
|
||||
* - 紧急故障修复
|
||||
* - 定期维护窗口
|
||||
*
|
||||
* @author kiro-ai
|
||||
* @version 1.0.0
|
||||
* @since 2025-12-24
|
||||
*/
|
||||
|
||||
import { Injectable, NestMiddleware } from '@nestjs/common';
|
||||
import { Request, Response, NextFunction } from 'express';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { Logger } from '@nestjs/common';
|
||||
|
||||
/**
|
||||
* 维护模式响应接口
|
||||
*/
|
||||
interface MaintenanceResponse {
|
||||
/** 请求是否成功 */
|
||||
success: boolean;
|
||||
/** 响应消息 */
|
||||
message: string;
|
||||
/** 错误代码 */
|
||||
error_code: string;
|
||||
/** 维护信息 */
|
||||
maintenance_info?: {
|
||||
/** 维护开始时间 */
|
||||
start_time: string;
|
||||
/** 预计结束时间 */
|
||||
estimated_end_time?: string;
|
||||
/** 重试间隔(秒) */
|
||||
retry_after: number;
|
||||
/** 维护原因 */
|
||||
reason?: string;
|
||||
};
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class MaintenanceMiddleware implements NestMiddleware {
|
||||
private readonly logger = new Logger(MaintenanceMiddleware.name);
|
||||
|
||||
constructor(private readonly configService: ConfigService) {}
|
||||
|
||||
/**
|
||||
* 中间件处理函数
|
||||
*
|
||||
* 业务逻辑:
|
||||
* 1. 检查维护模式环境变量
|
||||
* 2. 如果处于维护模式,返回503状态码
|
||||
* 3. 提供维护信息和重试建议
|
||||
* 4. 记录维护期间的访问尝试
|
||||
*
|
||||
* @param req HTTP请求对象
|
||||
* @param res HTTP响应对象
|
||||
* @param next 下一个中间件函数
|
||||
*/
|
||||
use(req: Request, res: Response, next: NextFunction) {
|
||||
// 1. 检查维护模式状态
|
||||
const isMaintenanceMode = this.configService.get<string>('MAINTENANCE_MODE') === 'true';
|
||||
|
||||
if (!isMaintenanceMode) {
|
||||
// 非维护模式,继续处理请求
|
||||
next();
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. 记录维护期间的访问尝试
|
||||
this.logger.warn('维护模式:拒绝访问请求', {
|
||||
operation: 'maintenance_check',
|
||||
method: req.method,
|
||||
url: req.url,
|
||||
userAgent: req.get('User-Agent'),
|
||||
ip: req.ip,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
// 3. 获取维护配置信息
|
||||
const maintenanceStartTime = this.configService.get<string>('MAINTENANCE_START_TIME') || new Date().toISOString();
|
||||
const maintenanceEndTime = this.configService.get<string>('MAINTENANCE_END_TIME');
|
||||
const maintenanceReason = this.configService.get<string>('MAINTENANCE_REASON') || '系统维护升级';
|
||||
const retryAfter = this.configService.get<number>('MAINTENANCE_RETRY_AFTER') || 1800; // 默认30分钟
|
||||
|
||||
// 4. 构建维护模式响应
|
||||
const maintenanceResponse: MaintenanceResponse = {
|
||||
success: false,
|
||||
message: '系统正在维护中,请稍后再试',
|
||||
error_code: 'SERVICE_UNAVAILABLE',
|
||||
maintenance_info: {
|
||||
start_time: maintenanceStartTime,
|
||||
estimated_end_time: maintenanceEndTime,
|
||||
retry_after: retryAfter,
|
||||
reason: maintenanceReason
|
||||
}
|
||||
};
|
||||
|
||||
// 5. 设置HTTP响应头
|
||||
res.setHeader('Retry-After', retryAfter.toString());
|
||||
res.setHeader('Content-Type', 'application/json');
|
||||
|
||||
// 6. 返回503服务不可用状态
|
||||
res.status(503).json(maintenanceResponse);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查维护模式是否启用
|
||||
*
|
||||
* @returns 是否处于维护模式
|
||||
*/
|
||||
isMaintenanceEnabled(): boolean {
|
||||
return this.configService.get<string>('MAINTENANCE_MODE') === 'true';
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取维护信息
|
||||
*
|
||||
* @returns 维护配置信息
|
||||
*/
|
||||
getMaintenanceInfo() {
|
||||
return {
|
||||
enabled: this.isMaintenanceEnabled(),
|
||||
startTime: this.configService.get<string>('MAINTENANCE_START_TIME'),
|
||||
endTime: this.configService.get<string>('MAINTENANCE_END_TIME'),
|
||||
reason: this.configService.get<string>('MAINTENANCE_REASON'),
|
||||
retryAfter: this.configService.get<number>('MAINTENANCE_RETRY_AFTER')
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
/**
|
||||
* 安全功能模块
|
||||
*
|
||||
* 功能描述:
|
||||
* - 整合所有安全相关功能
|
||||
* - 频率限制和请求超时控制
|
||||
* - 维护模式和内容类型验证
|
||||
* - 系统安全防护机制
|
||||
*
|
||||
* @author kiro-ai
|
||||
* @version 1.0.0
|
||||
* @since 2025-12-24
|
||||
*/
|
||||
|
||||
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';
|
||||
|
||||
@Module({
|
||||
providers: [
|
||||
ThrottleGuard,
|
||||
TimeoutInterceptor,
|
||||
// 全局频率限制守卫
|
||||
{
|
||||
provide: APP_GUARD,
|
||||
useClass: ThrottleGuard,
|
||||
},
|
||||
// 全局超时拦截器
|
||||
{
|
||||
provide: APP_INTERCEPTOR,
|
||||
useClass: TimeoutInterceptor,
|
||||
},
|
||||
],
|
||||
exports: [ThrottleGuard, TimeoutInterceptor],
|
||||
})
|
||||
export class SecurityModule {}
|
||||
@@ -20,8 +20,8 @@ import { Body, Controller, Get, HttpCode, HttpStatus, Param, Put, Post, UseGuard
|
||||
import { ApiBearerAuth, ApiBody, ApiOperation, ApiParam, ApiResponse, ApiTags } from '@nestjs/swagger';
|
||||
import { AdminGuard } from '../../admin/guards/admin.guard';
|
||||
import { UserManagementService } from '../services/user-management.service';
|
||||
import { Throttle, ThrottlePresets } from '../../security/decorators/throttle.decorator';
|
||||
import { Timeout, TimeoutPresets } from '../../security/decorators/timeout.decorator';
|
||||
import { Throttle, ThrottlePresets } from '../../../core/security_core/decorators/throttle.decorator';
|
||||
import { Timeout, TimeoutPresets } from '../../../core/security_core/decorators/timeout.decorator';
|
||||
import { UserStatusDto, BatchUserStatusDto } from '../dto/user-status.dto';
|
||||
import { UserStatusResponseDto, BatchUserStatusResponseDto, UserStatusStatsResponseDto } from '../dto/user-status-response.dto';
|
||||
|
||||
|
||||
Reference in New Issue
Block a user