forked from datawhale/whale-town-end
feat: 重构业务模块架构
- 新增auth模块处理认证逻辑 - 新增security模块处理安全相关功能 - 新增user-mgmt模块管理用户相关操作 - 新增shared模块存放共享组件 - 重构admin模块,添加DTO和Guards - 为admin模块添加测试文件结构
This commit is contained in:
224
src/business/security/middleware/content-type.middleware.ts
Normal file
224
src/business/security/middleware/content-type.middleware.ts
Normal file
@@ -0,0 +1,224 @@
|
||||
/**
|
||||
* 内容类型检查中间件
|
||||
*
|
||||
* 功能描述:
|
||||
* - 检查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);
|
||||
}
|
||||
}
|
||||
137
src/business/security/middleware/maintenance.middleware.ts
Normal file
137
src/business/security/middleware/maintenance.middleware.ts
Normal file
@@ -0,0 +1,137 @@
|
||||
/**
|
||||
* 维护模式中间件
|
||||
*
|
||||
* 功能描述:
|
||||
* - 检查系统是否处于维护模式
|
||||
* - 在维护期间阻止用户访问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')
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user