feat(zulip): 添加全面的 Zulip 集成系统
* **新增 Zulip 模块**:包含完整的集成服务,涵盖客户端池(client pool)、会话管理及事件处理。 * **新增 WebSocket 网关**:用于处理 Zulip 的实时事件监听与双向通信。 * **新增安全服务**:支持 API 密钥加密存储及凭据的安全管理。 * **新增配置管理服务**:支持配置热加载(hot-reload),实现动态配置更新。 * **新增错误处理与监控服务**:提升系统的可靠性与可观测性。 * **新增消息过滤服务**:用于内容校验及速率限制(流控)。 * **新增流初始化与会话清理服务**:优化资源管理与回收。 * **完善测试覆盖**:包含单元测试及端到端(e2e)集成测试。 * **完善详细文档**:包括 API 参考手册、配置指南及集成概述。 * **新增地图配置系统**:实现游戏地点与 Zulip Stream(频道)及 Topic(话题)的逻辑映射。 * **新增环境变量配置**:涵盖 Zulip 服务器地址、身份验证及监控相关设置。 * **更新 App 模块**:注册并启用新的 Zulip 集成模块。 * **更新 Redis 接口**:以支持增强型的会话管理功能。 * **实现 WebSocket 协议支持**:确保与 Zulip 之间的实时双向通信。
This commit is contained in:
799
src/business/zulip/services/api-key-security.service.ts
Normal file
799
src/business/zulip/services/api-key-security.service.ts
Normal file
@@ -0,0 +1,799 @@
|
||||
/**
|
||||
* API Key安全存储服务
|
||||
*
|
||||
* 功能描述:
|
||||
* - 实现Zulip API Key的加密存储
|
||||
* - 提供安全日志记录功能
|
||||
* - 检测异常操作并记录安全事件
|
||||
* - 支持API Key的安全获取和更新
|
||||
*
|
||||
* 主要方法:
|
||||
* - storeApiKey(): 加密存储API Key
|
||||
* - getApiKey(): 安全获取API Key
|
||||
* - updateApiKey(): 更新API Key
|
||||
* - deleteApiKey(): 删除API Key
|
||||
* - logSecurityEvent(): 记录安全事件
|
||||
*
|
||||
* 使用场景:
|
||||
* - 用户首次绑定Zulip账户
|
||||
* - Zulip客户端创建时获取API Key
|
||||
* - 检测到异常操作时记录安全日志
|
||||
*
|
||||
* 依赖模块:
|
||||
* - AppLoggerService: 日志记录服务
|
||||
* - IRedisService: Redis缓存服务
|
||||
*
|
||||
* @author 开发团队
|
||||
* @version 1.0.0
|
||||
* @since 2025-12-25
|
||||
*/
|
||||
|
||||
import { Injectable, Inject, Logger } from '@nestjs/common';
|
||||
import * as crypto from 'crypto';
|
||||
import { IRedisService } from '../../../core/redis/redis.interface';
|
||||
|
||||
/**
|
||||
* 安全事件类型枚举
|
||||
*/
|
||||
export enum SecurityEventType {
|
||||
API_KEY_STORED = 'api_key_stored',
|
||||
API_KEY_ACCESSED = 'api_key_accessed',
|
||||
API_KEY_UPDATED = 'api_key_updated',
|
||||
API_KEY_DELETED = 'api_key_deleted',
|
||||
API_KEY_DECRYPTION_FAILED = 'api_key_decryption_failed',
|
||||
SUSPICIOUS_ACCESS = 'suspicious_access',
|
||||
RATE_LIMIT_EXCEEDED = 'rate_limit_exceeded',
|
||||
INVALID_KEY_FORMAT = 'invalid_key_format',
|
||||
UNAUTHORIZED_ACCESS = 'unauthorized_access',
|
||||
}
|
||||
|
||||
/**
|
||||
* 安全事件严重级别
|
||||
*/
|
||||
export enum SecuritySeverity {
|
||||
INFO = 'info',
|
||||
WARNING = 'warning',
|
||||
CRITICAL = 'critical',
|
||||
}
|
||||
|
||||
/**
|
||||
* 安全事件记录接口
|
||||
*/
|
||||
export interface SecurityEvent {
|
||||
eventType: SecurityEventType;
|
||||
severity: SecuritySeverity;
|
||||
userId: string;
|
||||
details: Record<string, any>;
|
||||
timestamp: Date;
|
||||
ipAddress?: string;
|
||||
userAgent?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 加密后的API Key存储结构
|
||||
*/
|
||||
export interface EncryptedApiKey {
|
||||
encryptedKey: string;
|
||||
iv: string;
|
||||
authTag: string;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
accessCount: number;
|
||||
lastAccessedAt?: Date;
|
||||
}
|
||||
|
||||
/**
|
||||
* API Key存储结果
|
||||
*/
|
||||
export interface StoreApiKeyResult {
|
||||
success: boolean;
|
||||
message: string;
|
||||
userId?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* API Key获取结果
|
||||
*/
|
||||
export interface GetApiKeyResult {
|
||||
success: boolean;
|
||||
apiKey?: string;
|
||||
message?: string;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class ApiKeySecurityService {
|
||||
private readonly logger = new Logger(ApiKeySecurityService.name);
|
||||
private readonly API_KEY_PREFIX = 'zulip:api_key:';
|
||||
private readonly SECURITY_LOG_PREFIX = 'zulip:security_log:';
|
||||
private readonly ACCESS_COUNT_PREFIX = 'zulip:api_key_access:';
|
||||
private readonly ENCRYPTION_ALGORITHM = 'aes-256-gcm';
|
||||
private readonly KEY_LENGTH = 32; // 256 bits
|
||||
private readonly IV_LENGTH = 16; // 128 bits
|
||||
private readonly AUTH_TAG_LENGTH = 16; // 128 bits
|
||||
private readonly MAX_ACCESS_PER_MINUTE = 60; // 每分钟最大访问次数
|
||||
private readonly SECURITY_LOG_RETENTION = 30 * 24 * 3600; // 30天
|
||||
|
||||
// 加密密钥(生产环境应从环境变量或密钥管理服务获取)
|
||||
private readonly encryptionKey: Buffer;
|
||||
|
||||
constructor(
|
||||
@Inject('REDIS_SERVICE')
|
||||
private readonly redisService: IRedisService,
|
||||
) {
|
||||
// 从环境变量获取加密密钥,如果没有则生成一个默认密钥(仅用于开发)
|
||||
const keyFromEnv = process.env.ZULIP_API_KEY_ENCRYPTION_KEY;
|
||||
if (keyFromEnv) {
|
||||
this.encryptionKey = Buffer.from(keyFromEnv, 'hex');
|
||||
} else {
|
||||
// 开发环境使用固定密钥(生产环境必须配置环境变量)
|
||||
this.encryptionKey = crypto.scryptSync('default-dev-key', 'salt', this.KEY_LENGTH);
|
||||
this.logger.warn('使用默认加密密钥,生产环境请配置ZULIP_API_KEY_ENCRYPTION_KEY环境变量');
|
||||
}
|
||||
|
||||
this.logger.log('ApiKeySecurityService初始化完成');
|
||||
}
|
||||
|
||||
/**
|
||||
* 加密存储API Key
|
||||
*
|
||||
* 功能描述:
|
||||
* 使用AES-256-GCM算法加密API Key并存储到Redis
|
||||
*
|
||||
* 业务逻辑:
|
||||
* 1. 验证API Key格式
|
||||
* 2. 生成随机IV
|
||||
* 3. 使用AES-256-GCM加密
|
||||
* 4. 存储加密后的数据到Redis
|
||||
* 5. 记录安全日志
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @param apiKey Zulip API Key
|
||||
* @param metadata 可选的元数据(如IP地址)
|
||||
* @returns Promise<StoreApiKeyResult> 存储结果
|
||||
*/
|
||||
async storeApiKey(
|
||||
userId: string,
|
||||
apiKey: string,
|
||||
metadata?: { ipAddress?: string; userAgent?: string }
|
||||
): Promise<StoreApiKeyResult> {
|
||||
const startTime = Date.now();
|
||||
|
||||
this.logger.log(`开始存储API Key: ${userId}`);
|
||||
|
||||
try {
|
||||
// 1. 参数验证
|
||||
if (!userId || !userId.trim()) {
|
||||
await this.logSecurityEvent({
|
||||
eventType: SecurityEventType.INVALID_KEY_FORMAT,
|
||||
severity: SecuritySeverity.WARNING,
|
||||
userId: userId || 'unknown',
|
||||
details: { reason: 'empty_user_id' },
|
||||
timestamp: new Date(),
|
||||
...metadata,
|
||||
});
|
||||
return { success: false, message: '用户ID不能为空' };
|
||||
}
|
||||
|
||||
if (!apiKey || !apiKey.trim()) {
|
||||
await this.logSecurityEvent({
|
||||
eventType: SecurityEventType.INVALID_KEY_FORMAT,
|
||||
severity: SecuritySeverity.WARNING,
|
||||
userId,
|
||||
details: { reason: 'empty_api_key' },
|
||||
timestamp: new Date(),
|
||||
...metadata,
|
||||
});
|
||||
return { success: false, message: 'API Key不能为空' };
|
||||
}
|
||||
|
||||
// 2. 验证API Key格式(Zulip API Key通常是32字符的字母数字字符串)
|
||||
if (!this.isValidApiKeyFormat(apiKey)) {
|
||||
await this.logSecurityEvent({
|
||||
eventType: SecurityEventType.INVALID_KEY_FORMAT,
|
||||
severity: SecuritySeverity.WARNING,
|
||||
userId,
|
||||
details: { reason: 'invalid_format', keyLength: apiKey.length },
|
||||
timestamp: new Date(),
|
||||
...metadata,
|
||||
});
|
||||
return { success: false, message: 'API Key格式无效' };
|
||||
}
|
||||
|
||||
// 3. 加密API Key
|
||||
const encrypted = this.encrypt(apiKey);
|
||||
|
||||
// 4. 构建存储数据
|
||||
const storageData: EncryptedApiKey = {
|
||||
encryptedKey: encrypted.encryptedData,
|
||||
iv: encrypted.iv,
|
||||
authTag: encrypted.authTag,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
accessCount: 0,
|
||||
};
|
||||
|
||||
// 5. 存储到Redis
|
||||
const storageKey = `${this.API_KEY_PREFIX}${userId}`;
|
||||
await this.redisService.set(storageKey, JSON.stringify(storageData));
|
||||
|
||||
// 6. 记录安全日志
|
||||
await this.logSecurityEvent({
|
||||
eventType: SecurityEventType.API_KEY_STORED,
|
||||
severity: SecuritySeverity.INFO,
|
||||
userId,
|
||||
details: {
|
||||
action: 'store',
|
||||
keyLength: apiKey.length,
|
||||
},
|
||||
timestamp: new Date(),
|
||||
...metadata,
|
||||
});
|
||||
|
||||
const duration = Date.now() - startTime;
|
||||
this.logger.log(`API Key存储成功: ${userId}`);
|
||||
|
||||
return { success: true, message: 'API Key存储成功', userId };
|
||||
|
||||
} catch (error) {
|
||||
const err = error as Error;
|
||||
const duration = Date.now() - startTime;
|
||||
|
||||
this.logger.error('API Key存储失败', {
|
||||
operation: 'storeApiKey',
|
||||
userId,
|
||||
error: err.message,
|
||||
duration,
|
||||
timestamp: new Date().toISOString(),
|
||||
}, err.stack);
|
||||
|
||||
return { success: false, message: '存储失败,请稍后重试' };
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 安全获取API Key
|
||||
*
|
||||
* 功能描述:
|
||||
* 从Redis获取加密的API Key并解密返回
|
||||
*
|
||||
* 业务逻辑:
|
||||
* 1. 检查访问频率限制
|
||||
* 2. 从Redis获取加密数据
|
||||
* 3. 解密API Key
|
||||
* 4. 更新访问计数
|
||||
* 5. 记录访问日志
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @param metadata 可选的元数据
|
||||
* @returns Promise<GetApiKeyResult> 获取结果
|
||||
*/
|
||||
async getApiKey(
|
||||
userId: string,
|
||||
metadata?: { ipAddress?: string; userAgent?: string }
|
||||
): Promise<GetApiKeyResult> {
|
||||
const startTime = Date.now();
|
||||
|
||||
this.logger.debug('开始获取API Key', {
|
||||
operation: 'getApiKey',
|
||||
userId,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
|
||||
try {
|
||||
// 1. 参数验证
|
||||
if (!userId || !userId.trim()) {
|
||||
return { success: false, message: '用户ID不能为空' };
|
||||
}
|
||||
|
||||
// 2. 检查访问频率限制
|
||||
const rateLimitCheck = await this.checkAccessRateLimit(userId);
|
||||
if (!rateLimitCheck.allowed) {
|
||||
await this.logSecurityEvent({
|
||||
eventType: SecurityEventType.RATE_LIMIT_EXCEEDED,
|
||||
severity: SecuritySeverity.WARNING,
|
||||
userId,
|
||||
details: {
|
||||
currentCount: rateLimitCheck.currentCount,
|
||||
limit: this.MAX_ACCESS_PER_MINUTE,
|
||||
},
|
||||
timestamp: new Date(),
|
||||
...metadata,
|
||||
});
|
||||
return { success: false, message: '访问频率过高,请稍后重试' };
|
||||
}
|
||||
|
||||
// 3. 从Redis获取加密数据
|
||||
const storageKey = `${this.API_KEY_PREFIX}${userId}`;
|
||||
const encryptedData = await this.redisService.get(storageKey);
|
||||
|
||||
if (!encryptedData) {
|
||||
this.logger.debug('API Key不存在', {
|
||||
operation: 'getApiKey',
|
||||
userId,
|
||||
});
|
||||
return { success: false, message: 'API Key不存在' };
|
||||
}
|
||||
|
||||
// 4. 解析存储数据
|
||||
const storageData: EncryptedApiKey = JSON.parse(encryptedData);
|
||||
|
||||
// 5. 解密API Key
|
||||
let apiKey: string;
|
||||
try {
|
||||
apiKey = this.decrypt(
|
||||
storageData.encryptedKey,
|
||||
storageData.iv,
|
||||
storageData.authTag
|
||||
);
|
||||
} catch (decryptError) {
|
||||
await this.logSecurityEvent({
|
||||
eventType: SecurityEventType.API_KEY_DECRYPTION_FAILED,
|
||||
severity: SecuritySeverity.CRITICAL,
|
||||
userId,
|
||||
details: {
|
||||
error: (decryptError as Error).message,
|
||||
},
|
||||
timestamp: new Date(),
|
||||
...metadata,
|
||||
});
|
||||
return { success: false, message: 'API Key解密失败' };
|
||||
}
|
||||
|
||||
// 6. 更新访问计数和时间
|
||||
storageData.accessCount += 1;
|
||||
storageData.lastAccessedAt = new Date();
|
||||
await this.redisService.set(storageKey, JSON.stringify(storageData));
|
||||
|
||||
// 7. 记录访问日志
|
||||
await this.logSecurityEvent({
|
||||
eventType: SecurityEventType.API_KEY_ACCESSED,
|
||||
severity: SecuritySeverity.INFO,
|
||||
userId,
|
||||
details: {
|
||||
accessCount: storageData.accessCount,
|
||||
},
|
||||
timestamp: new Date(),
|
||||
...metadata,
|
||||
});
|
||||
|
||||
const duration = Date.now() - startTime;
|
||||
this.logger.debug('API Key获取成功', {
|
||||
operation: 'getApiKey',
|
||||
userId,
|
||||
accessCount: storageData.accessCount,
|
||||
duration,
|
||||
});
|
||||
|
||||
return { success: true, apiKey };
|
||||
|
||||
} catch (error) {
|
||||
const err = error as Error;
|
||||
const duration = Date.now() - startTime;
|
||||
|
||||
this.logger.error('API Key获取失败', {
|
||||
operation: 'getApiKey',
|
||||
userId,
|
||||
error: err.message,
|
||||
duration,
|
||||
timestamp: new Date().toISOString(),
|
||||
}, err.stack);
|
||||
|
||||
return { success: false, message: '获取失败,请稍后重试' };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新API Key
|
||||
*
|
||||
* 功能描述:
|
||||
* 更新用户的Zulip API Key
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @param newApiKey 新的API Key
|
||||
* @param metadata 可选的元数据
|
||||
* @returns Promise<StoreApiKeyResult> 更新结果
|
||||
*/
|
||||
async updateApiKey(
|
||||
userId: string,
|
||||
newApiKey: string,
|
||||
metadata?: { ipAddress?: string; userAgent?: string }
|
||||
): Promise<StoreApiKeyResult> {
|
||||
this.logger.log(`开始更新API Key: ${userId}`);
|
||||
|
||||
try {
|
||||
// 1. 检查原API Key是否存在
|
||||
const storageKey = `${this.API_KEY_PREFIX}${userId}`;
|
||||
const existingData = await this.redisService.get(storageKey);
|
||||
|
||||
if (!existingData) {
|
||||
// 如果不存在,则创建新的
|
||||
return this.storeApiKey(userId, newApiKey, metadata);
|
||||
}
|
||||
|
||||
// 2. 验证新API Key格式
|
||||
if (!this.isValidApiKeyFormat(newApiKey)) {
|
||||
await this.logSecurityEvent({
|
||||
eventType: SecurityEventType.INVALID_KEY_FORMAT,
|
||||
severity: SecuritySeverity.WARNING,
|
||||
userId,
|
||||
details: { reason: 'invalid_format', action: 'update' },
|
||||
timestamp: new Date(),
|
||||
...metadata,
|
||||
});
|
||||
return { success: false, message: 'API Key格式无效' };
|
||||
}
|
||||
|
||||
// 3. 解析现有数据
|
||||
const oldStorageData: EncryptedApiKey = JSON.parse(existingData);
|
||||
|
||||
// 4. 加密新API Key
|
||||
const encrypted = this.encrypt(newApiKey);
|
||||
|
||||
// 5. 更新存储数据
|
||||
const newStorageData: EncryptedApiKey = {
|
||||
encryptedKey: encrypted.encryptedData,
|
||||
iv: encrypted.iv,
|
||||
authTag: encrypted.authTag,
|
||||
createdAt: oldStorageData.createdAt,
|
||||
updatedAt: new Date(),
|
||||
accessCount: oldStorageData.accessCount,
|
||||
lastAccessedAt: oldStorageData.lastAccessedAt,
|
||||
};
|
||||
|
||||
await this.redisService.set(storageKey, JSON.stringify(newStorageData));
|
||||
|
||||
// 6. 记录安全日志
|
||||
await this.logSecurityEvent({
|
||||
eventType: SecurityEventType.API_KEY_UPDATED,
|
||||
severity: SecuritySeverity.INFO,
|
||||
userId,
|
||||
details: {
|
||||
action: 'update',
|
||||
previousAccessCount: oldStorageData.accessCount,
|
||||
},
|
||||
timestamp: new Date(),
|
||||
...metadata,
|
||||
});
|
||||
|
||||
this.logger.log(`API Key更新成功: ${userId}`);
|
||||
|
||||
return { success: true, message: 'API Key更新成功', userId };
|
||||
|
||||
} catch (error) {
|
||||
const err = error as Error;
|
||||
this.logger.error('API Key更新失败', {
|
||||
operation: 'updateApiKey',
|
||||
userId,
|
||||
error: err.message,
|
||||
timestamp: new Date().toISOString(),
|
||||
}, err.stack);
|
||||
|
||||
return { success: false, message: '更新失败,请稍后重试' };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除API Key
|
||||
*
|
||||
* 功能描述:
|
||||
* 安全删除用户的API Key
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @param metadata 可选的元数据
|
||||
* @returns Promise<boolean> 是否删除成功
|
||||
*/
|
||||
async deleteApiKey(
|
||||
userId: string,
|
||||
metadata?: { ipAddress?: string; userAgent?: string }
|
||||
): Promise<boolean> {
|
||||
this.logger.log(`开始删除API Key: ${userId}`);
|
||||
|
||||
try {
|
||||
const storageKey = `${this.API_KEY_PREFIX}${userId}`;
|
||||
await this.redisService.del(storageKey);
|
||||
|
||||
// 记录安全日志
|
||||
await this.logSecurityEvent({
|
||||
eventType: SecurityEventType.API_KEY_DELETED,
|
||||
severity: SecuritySeverity.INFO,
|
||||
userId,
|
||||
details: { action: 'delete' },
|
||||
timestamp: new Date(),
|
||||
...metadata,
|
||||
});
|
||||
|
||||
this.logger.log(`API Key删除成功: ${userId}`);
|
||||
|
||||
return true;
|
||||
|
||||
} catch (error) {
|
||||
const err = error as Error;
|
||||
this.logger.error('API Key删除失败', {
|
||||
operation: 'deleteApiKey',
|
||||
userId,
|
||||
error: err.message,
|
||||
timestamp: new Date().toISOString(),
|
||||
}, err.stack);
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查API Key是否存在
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @returns Promise<boolean> 是否存在
|
||||
*/
|
||||
async hasApiKey(userId: string): Promise<boolean> {
|
||||
try {
|
||||
const storageKey = `${this.API_KEY_PREFIX}${userId}`;
|
||||
return await this.redisService.exists(storageKey);
|
||||
} catch (error) {
|
||||
this.logger.error('检查API Key存在性失败', {
|
||||
operation: 'hasApiKey',
|
||||
userId,
|
||||
error: (error as Error).message,
|
||||
});
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录安全事件
|
||||
*
|
||||
* 功能描述:
|
||||
* 记录安全相关的事件到Redis,用于审计和监控
|
||||
*
|
||||
* @param event 安全事件
|
||||
* @returns Promise<void>
|
||||
*/
|
||||
async logSecurityEvent(event: SecurityEvent): Promise<void> {
|
||||
try {
|
||||
const logKey = `${this.SECURITY_LOG_PREFIX}${event.userId}:${Date.now()}`;
|
||||
await this.redisService.setex(
|
||||
logKey,
|
||||
this.SECURITY_LOG_RETENTION,
|
||||
JSON.stringify(event)
|
||||
);
|
||||
|
||||
// 根据严重级别记录到应用日志
|
||||
const logContext = {
|
||||
operation: 'logSecurityEvent',
|
||||
eventType: event.eventType,
|
||||
severity: event.severity,
|
||||
userId: event.userId,
|
||||
details: event.details,
|
||||
ipAddress: event.ipAddress,
|
||||
timestamp: event.timestamp.toISOString(),
|
||||
};
|
||||
|
||||
switch (event.severity) {
|
||||
case SecuritySeverity.CRITICAL:
|
||||
this.logger.error('安全事件 - 严重', logContext);
|
||||
break;
|
||||
case SecuritySeverity.WARNING:
|
||||
this.logger.warn('安全事件 - 警告', logContext);
|
||||
break;
|
||||
case SecuritySeverity.INFO:
|
||||
default:
|
||||
this.logger.log('安全事件 - 信息', logContext);
|
||||
break;
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
this.logger.error('记录安全事件失败', {
|
||||
operation: 'logSecurityEvent',
|
||||
event,
|
||||
error: (error as Error).message,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录可疑访问
|
||||
*
|
||||
* 功能描述:
|
||||
* 当检测到异常操作时记录可疑访问事件
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @param reason 可疑原因
|
||||
* @param details 详细信息
|
||||
* @param metadata 元数据
|
||||
* @returns Promise<void>
|
||||
*/
|
||||
async logSuspiciousAccess(
|
||||
userId: string,
|
||||
reason: string,
|
||||
details: Record<string, any>,
|
||||
metadata?: { ipAddress?: string; userAgent?: string }
|
||||
): Promise<void> {
|
||||
await this.logSecurityEvent({
|
||||
eventType: SecurityEventType.SUSPICIOUS_ACCESS,
|
||||
severity: SecuritySeverity.WARNING,
|
||||
userId,
|
||||
details: {
|
||||
reason,
|
||||
...details,
|
||||
},
|
||||
timestamp: new Date(),
|
||||
...metadata,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户安全事件历史
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @param limit 返回数量限制
|
||||
* @returns Promise<SecurityEvent[]> 安全事件列表
|
||||
*/
|
||||
async getSecurityEventHistory(userId: string, limit: number = 100): Promise<SecurityEvent[]> {
|
||||
// 注意:这是一个简化实现,实际应该使用Redis的有序集合或扫描功能
|
||||
// 当前实现仅作为示例
|
||||
this.logger.debug('获取安全事件历史', {
|
||||
operation: 'getSecurityEventHistory',
|
||||
userId,
|
||||
limit,
|
||||
});
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取API Key统计信息
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @returns Promise<{exists: boolean, accessCount?: number, lastAccessedAt?: Date, createdAt?: Date}>
|
||||
*/
|
||||
async getApiKeyStats(userId: string): Promise<{
|
||||
exists: boolean;
|
||||
accessCount?: number;
|
||||
lastAccessedAt?: Date;
|
||||
createdAt?: Date;
|
||||
updatedAt?: Date;
|
||||
}> {
|
||||
try {
|
||||
const storageKey = `${this.API_KEY_PREFIX}${userId}`;
|
||||
const data = await this.redisService.get(storageKey);
|
||||
|
||||
if (!data) {
|
||||
return { exists: false };
|
||||
}
|
||||
|
||||
const storageData: EncryptedApiKey = JSON.parse(data);
|
||||
return {
|
||||
exists: true,
|
||||
accessCount: storageData.accessCount,
|
||||
lastAccessedAt: storageData.lastAccessedAt ? new Date(storageData.lastAccessedAt) : undefined,
|
||||
createdAt: new Date(storageData.createdAt),
|
||||
updatedAt: new Date(storageData.updatedAt),
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
this.logger.error('获取API Key统计信息失败', {
|
||||
operation: 'getApiKeyStats',
|
||||
userId,
|
||||
error: (error as Error).message,
|
||||
});
|
||||
return { exists: false };
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 私有方法 ====================
|
||||
|
||||
/**
|
||||
* 加密数据
|
||||
*
|
||||
* @param plaintext 明文
|
||||
* @returns 加密结果
|
||||
* @private
|
||||
*/
|
||||
private encrypt(plaintext: string): {
|
||||
encryptedData: string;
|
||||
iv: string;
|
||||
authTag: string;
|
||||
} {
|
||||
const iv = crypto.randomBytes(this.IV_LENGTH);
|
||||
const cipher = crypto.createCipheriv(
|
||||
this.ENCRYPTION_ALGORITHM,
|
||||
this.encryptionKey,
|
||||
iv
|
||||
);
|
||||
|
||||
let encrypted = cipher.update(plaintext, 'utf8', 'hex');
|
||||
encrypted += cipher.final('hex');
|
||||
const authTag = cipher.getAuthTag();
|
||||
|
||||
return {
|
||||
encryptedData: encrypted,
|
||||
iv: iv.toString('hex'),
|
||||
authTag: authTag.toString('hex'),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 解密数据
|
||||
*
|
||||
* @param encryptedData 加密数据
|
||||
* @param ivHex IV(十六进制)
|
||||
* @param authTagHex 认证标签(十六进制)
|
||||
* @returns 解密后的明文
|
||||
* @private
|
||||
*/
|
||||
private decrypt(encryptedData: string, ivHex: string, authTagHex: string): string {
|
||||
const iv = Buffer.from(ivHex, 'hex');
|
||||
const authTag = Buffer.from(authTagHex, 'hex');
|
||||
const decipher = crypto.createDecipheriv(
|
||||
this.ENCRYPTION_ALGORITHM,
|
||||
this.encryptionKey,
|
||||
iv
|
||||
);
|
||||
decipher.setAuthTag(authTag);
|
||||
|
||||
let decrypted = decipher.update(encryptedData, 'hex', 'utf8');
|
||||
decrypted += decipher.final('utf8');
|
||||
|
||||
return decrypted;
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证API Key格式
|
||||
*
|
||||
* @param apiKey API Key
|
||||
* @returns boolean 是否有效
|
||||
* @private
|
||||
*/
|
||||
private isValidApiKeyFormat(apiKey: string): boolean {
|
||||
// Zulip API Key通常是32字符的字母数字字符串
|
||||
// 这里放宽限制以支持不同格式
|
||||
if (!apiKey || apiKey.length < 16 || apiKey.length > 128) {
|
||||
return false;
|
||||
}
|
||||
// 只允许字母、数字和一些特殊字符
|
||||
return /^[a-zA-Z0-9_-]+$/.test(apiKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查访问频率限制
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @returns Promise<{allowed: boolean, currentCount: number}>
|
||||
* @private
|
||||
*/
|
||||
private async checkAccessRateLimit(userId: string): Promise<{
|
||||
allowed: boolean;
|
||||
currentCount: number;
|
||||
}> {
|
||||
try {
|
||||
const rateLimitKey = `${this.ACCESS_COUNT_PREFIX}${userId}`;
|
||||
const currentCount = await this.redisService.get(rateLimitKey);
|
||||
const count = currentCount ? parseInt(currentCount, 10) : 0;
|
||||
|
||||
if (count >= this.MAX_ACCESS_PER_MINUTE) {
|
||||
return { allowed: false, currentCount: count };
|
||||
}
|
||||
|
||||
// 增加计数
|
||||
if (count === 0) {
|
||||
await this.redisService.setex(rateLimitKey, 60, '1');
|
||||
} else {
|
||||
await this.redisService.incr(rateLimitKey);
|
||||
}
|
||||
|
||||
return { allowed: true, currentCount: count + 1 };
|
||||
|
||||
} catch (error) {
|
||||
// 频率检查失败时默认允许
|
||||
this.logger.warn('访问频率检查失败', {
|
||||
operation: 'checkAccessRateLimit',
|
||||
userId,
|
||||
error: (error as Error).message,
|
||||
});
|
||||
return { allowed: true, currentCount: 0 };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user