/** * 验证码管理服务 * * 功能描述: * - 生成和管理各种类型的验证码 * - 使用Redis缓存验证码,支持过期时间 * - 提供验证码验证和防刷机制 * * 支持的验证码类型: * - 邮箱验证码 * - 密码重置验证码 * - 手机短信验证码 * * @author moyin * @version 1.0.0 * @since 2025-12-17 */ import { Injectable, Logger, BadRequestException, HttpException, HttpStatus, Inject } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { IRedisService } from '../../redis/redis.interface'; /** * 验证码类型枚举 */ export enum VerificationCodeType { EMAIL_VERIFICATION = 'email_verification', PASSWORD_RESET = 'password_reset', SMS_VERIFICATION = 'sms_verification', } /** * 验证码信息接口 */ export interface VerificationCodeInfo { /** 验证码 */ code: string; /** 创建时间 */ createdAt: number; /** 尝试次数 */ attempts: number; /** 最大尝试次数 */ maxAttempts: number; } @Injectable() export class VerificationService { private readonly logger = new Logger(VerificationService.name); // 验证码配置 private readonly CODE_LENGTH = 6; private readonly CODE_EXPIRE_TIME = 5 * 60; // 5分钟 private readonly MAX_ATTEMPTS = 3; // 最大验证尝试次数 private readonly RATE_LIMIT_TIME = 60; // 发送频率限制(秒) private readonly MAX_SENDS_PER_HOUR = 5; // 每小时最大发送次数 constructor( private readonly configService: ConfigService, @Inject('REDIS_SERVICE') private readonly redis: IRedisService, ) {} /** * 生成验证码 * * @param identifier 标识符(邮箱或手机号) * @param type 验证码类型 * @returns 验证码 */ async generateCode(identifier: string, type: VerificationCodeType): Promise { // 检查发送频率限制 await this.checkRateLimit(identifier, type); // 生成6位数字验证码 const code = this.generateRandomCode(); // 构建Redis键 const key = this.buildRedisKey(identifier, type); // 验证码信息 const codeInfo: VerificationCodeInfo = { code, createdAt: Date.now(), attempts: 0, maxAttempts: this.MAX_ATTEMPTS, }; // 存储到Redis,设置过期时间 await this.redis.set(key, JSON.stringify(codeInfo), this.CODE_EXPIRE_TIME); // 记录发送次数(用于频率限制) await this.recordSendAttempt(identifier, type); this.logger.log(`验证码已生成: ${identifier} (${type})`); return code; } /** * 验证验证码 * * @param identifier 标识符 * @param type 验证码类型 * @param inputCode 用户输入的验证码 * @returns 验证结果 */ async verifyCode(identifier: string, type: VerificationCodeType, inputCode: string): Promise { const key = this.buildRedisKey(identifier, type); // 从Redis获取验证码信息 const codeInfoStr = await this.redis.get(key); if (!codeInfoStr) { throw new BadRequestException('验证码不存在或已过期'); } const codeInfo: VerificationCodeInfo = JSON.parse(codeInfoStr); // 检查尝试次数 if (codeInfo.attempts >= codeInfo.maxAttempts) { await this.redis.del(key); throw new BadRequestException('验证码尝试次数过多,请重新获取'); } // 增加尝试次数 codeInfo.attempts++; await this.redis.set(key, JSON.stringify(codeInfo), this.CODE_EXPIRE_TIME); // 验证验证码 if (codeInfo.code !== inputCode) { this.logger.warn(`验证码验证失败: ${identifier} (${type}) - 剩余尝试次数: ${codeInfo.maxAttempts - codeInfo.attempts}`); throw new BadRequestException(`验证码错误,剩余尝试次数: ${codeInfo.maxAttempts - codeInfo.attempts}`); } // 验证成功,删除验证码 await this.redis.del(key); this.logger.log(`验证码验证成功: ${identifier} (${type})`); return true; } /** * 检查验证码是否存在 * * @param identifier 标识符 * @param type 验证码类型 * @returns 是否存在 */ async codeExists(identifier: string, type: VerificationCodeType): Promise { const key = this.buildRedisKey(identifier, type); return await this.redis.exists(key); } /** * 删除验证码 * * @param identifier 标识符 * @param type 验证码类型 */ async deleteCode(identifier: string, type: VerificationCodeType): Promise { const key = this.buildRedisKey(identifier, type); await this.redis.del(key); this.logger.log(`验证码已删除: ${identifier} (${type})`); } /** * 获取验证码剩余时间 * * @param identifier 标识符 * @param type 验证码类型 * @returns 剩余时间(秒),-1表示不存在 */ async getCodeTTL(identifier: string, type: VerificationCodeType): Promise { const key = this.buildRedisKey(identifier, type); return await this.redis.ttl(key); } /** * 检查发送频率限制 * * @param identifier 标识符 * @param type 验证码类型 */ private async checkRateLimit(identifier: string, type: VerificationCodeType): Promise { // 检查是否在冷却时间内 const cooldownKey = this.buildCooldownKey(identifier, type); const cooldownExists = await this.redis.exists(cooldownKey); if (cooldownExists) { const ttl = await this.redis.ttl(cooldownKey); throw new HttpException(`请等待 ${ttl} 秒后再试`, HttpStatus.TOO_MANY_REQUESTS); } // 检查每小时发送次数限制 const hourlyKey = this.buildHourlyKey(identifier, type); const hourlyCount = await this.redis.get(hourlyKey); if (hourlyCount && parseInt(hourlyCount) >= this.MAX_SENDS_PER_HOUR) { throw new HttpException('每小时发送次数已达上限,请稍后再试', HttpStatus.TOO_MANY_REQUESTS); } } /** * 记录发送尝试 * * @param identifier 标识符 * @param type 验证码类型 */ private async recordSendAttempt(identifier: string, type: VerificationCodeType): Promise { // 设置冷却时间 const cooldownKey = this.buildCooldownKey(identifier, type); await this.redis.set(cooldownKey, '1', this.RATE_LIMIT_TIME); // 记录每小时发送次数 const hourlyKey = this.buildHourlyKey(identifier, type); const current = await this.redis.get(hourlyKey); if (current) { const newCount = (parseInt(current) + 1).toString(); await this.redis.set(hourlyKey, newCount, 3600); } else { await this.redis.set(hourlyKey, '1', 3600); // 1小时过期 } } /** * 生成随机验证码 * * @returns 验证码 */ private generateRandomCode(): string { return Math.floor(Math.random() * Math.pow(10, this.CODE_LENGTH)) .toString() .padStart(this.CODE_LENGTH, '0'); } /** * 构建Redis键 * * @param identifier 标识符 * @param type 验证码类型 * @returns Redis键 */ private buildRedisKey(identifier: string, type: VerificationCodeType): string { return `verification_code:${type}:${identifier}`; } /** * 构建冷却时间Redis键 * * @param identifier 标识符 * @param type 验证码类型 * @returns Redis键 */ private buildCooldownKey(identifier: string, type: VerificationCodeType): string { return `verification_cooldown:${type}:${identifier}`; } /** * 构建每小时限制Redis键 * * @param identifier 标识符 * @param type 验证码类型 * @returns Redis键 */ private buildHourlyKey(identifier: string, type: VerificationCodeType): string { const hour = new Date().getHours(); const date = new Date().toDateString(); return `verification_hourly:${type}:${identifier}:${date}:${hour}`; } /** * 清理过期的验证码(可选的定时任务) */ async cleanupExpiredCodes(): Promise { // Redis会自动清理过期的键,这里可以添加额外的清理逻辑 this.logger.log('验证码清理任务执行完成'); } /** * 获取验证码统计信息 * * @param identifier 标识符 * @param type 验证码类型 * @returns 统计信息 */ async getCodeStats(identifier: string, type: VerificationCodeType): Promise<{ exists: boolean; ttl: number; attempts?: number; maxAttempts?: number; }> { const key = this.buildRedisKey(identifier, type); const exists = await this.redis.exists(key); const ttl = await this.redis.ttl(key); if (!exists) { return { exists: false, ttl: -1 }; } const codeInfoStr = await this.redis.get(key); let codeInfo: VerificationCodeInfo; try { codeInfo = JSON.parse(codeInfoStr || '{}'); } catch (error) { this.logger.error('验证码信息解析失败', error); codeInfo = {} as VerificationCodeInfo; } return { exists: true, ttl, attempts: codeInfo.attempts, maxAttempts: codeInfo.maxAttempts, }; } }