Merge pull request 'fix: 修复验证码验证时TTL重置导致过期的关键问题' (#7) from fix/verification-code-ttl-reset into main

Reviewed-on: datawhale/whale-town-end#7
This commit is contained in:
2025-12-17 21:24:24 +08:00
5 changed files with 249 additions and 7 deletions

112
Test-Verification-Debug.ps1 Normal file
View File

@@ -0,0 +1,112 @@
# 验证码问题调试脚本
# 作者: moyin
# 日期: 2025-12-17
$baseUrl = "http://localhost:3000"
$testEmail = "debug@example.com"
Write-Host "=== 验证码问题调试脚本 ===" -ForegroundColor Green
# 步骤1: 发送验证码
Write-Host "`n1. 发送验证码..." -ForegroundColor Yellow
$sendBody = @{
email = $testEmail
} | ConvertTo-Json
try {
$sendResponse = Invoke-RestMethod -Uri "$baseUrl/auth/send-email-verification" -Method POST -Body $sendBody -ContentType "application/json"
Write-Host "发送响应: $($sendResponse | ConvertTo-Json -Depth 3)" -ForegroundColor Cyan
if ($sendResponse.success) {
Write-Host "✅ 验证码发送成功" -ForegroundColor Green
# 步骤2: 立即查看验证码调试信息
Write-Host "`n2. 查看验证码调试信息..." -ForegroundColor Yellow
$debugResponse = Invoke-RestMethod -Uri "$baseUrl/auth/debug-verification-code" -Method POST -Body $sendBody -ContentType "application/json"
Write-Host "调试信息: $($debugResponse | ConvertTo-Json -Depth 5)" -ForegroundColor Cyan
# 步骤3: 故意输入错误验证码
Write-Host "`n3. 测试错误验证码..." -ForegroundColor Yellow
$wrongVerifyBody = @{
email = $testEmail
verification_code = "000000"
} | ConvertTo-Json
try {
$wrongResponse = Invoke-RestMethod -Uri "$baseUrl/auth/verify-email" -Method POST -Body $wrongVerifyBody -ContentType "application/json"
Write-Host "错误验证响应: $($wrongResponse | ConvertTo-Json -Depth 3)" -ForegroundColor Red
} catch {
Write-Host "错误验证失败(预期): $($_.Exception.Message)" -ForegroundColor Yellow
if ($_.Exception.Response) {
$errorResponse = $_.Exception.Response.GetResponseStream()
$reader = New-Object System.IO.StreamReader($errorResponse)
$errorBody = $reader.ReadToEnd()
Write-Host "错误详情: $errorBody" -ForegroundColor Red
}
}
# 步骤4: 再次查看调试信息
Write-Host "`n4. 错误验证后的调试信息..." -ForegroundColor Yellow
$debugResponse2 = Invoke-RestMethod -Uri "$baseUrl/auth/debug-verification-code" -Method POST -Body $sendBody -ContentType "application/json"
Write-Host "调试信息: $($debugResponse2 | ConvertTo-Json -Depth 5)" -ForegroundColor Cyan
# 步骤5: 再次尝试错误验证码
Write-Host "`n5. 再次测试错误验证码..." -ForegroundColor Yellow
try {
$wrongResponse2 = Invoke-RestMethod -Uri "$baseUrl/auth/verify-email" -Method POST -Body $wrongVerifyBody -ContentType "application/json"
Write-Host "第二次错误验证响应: $($wrongResponse2 | ConvertTo-Json -Depth 3)" -ForegroundColor Red
} catch {
Write-Host "第二次错误验证失败: $($_.Exception.Message)" -ForegroundColor Yellow
if ($_.Exception.Response) {
$errorResponse = $_.Exception.Response.GetResponseStream()
$reader = New-Object System.IO.StreamReader($errorResponse)
$errorBody = $reader.ReadToEnd()
Write-Host "错误详情: $errorBody" -ForegroundColor Red
}
}
# 步骤6: 最终调试信息
Write-Host "`n6. 最终调试信息..." -ForegroundColor Yellow
$debugResponse3 = Invoke-RestMethod -Uri "$baseUrl/auth/debug-verification-code" -Method POST -Body $sendBody -ContentType "application/json"
Write-Host "最终调试信息: $($debugResponse3 | ConvertTo-Json -Depth 5)" -ForegroundColor Cyan
# 步骤7: 使用正确验证码(如果有的话)
if ($sendResponse.data.verification_code) {
Write-Host "`n7. 使用正确验证码..." -ForegroundColor Yellow
$correctVerifyBody = @{
email = $testEmail
verification_code = $sendResponse.data.verification_code
} | ConvertTo-Json
try {
$correctResponse = Invoke-RestMethod -Uri "$baseUrl/auth/verify-email" -Method POST -Body $correctVerifyBody -ContentType "application/json"
Write-Host "正确验证响应: $($correctResponse | ConvertTo-Json -Depth 3)" -ForegroundColor Green
} catch {
Write-Host "正确验证也失败了: $($_.Exception.Message)" -ForegroundColor Red
if ($_.Exception.Response) {
$errorResponse = $_.Exception.Response.GetResponseStream()
$reader = New-Object System.IO.StreamReader($errorResponse)
$errorBody = $reader.ReadToEnd()
Write-Host "错误详情: $errorBody" -ForegroundColor Red
}
}
}
} else {
Write-Host "❌ 验证码发送失败: $($sendResponse.message)" -ForegroundColor Red
}
} catch {
Write-Host "❌ 请求失败: $($_.Exception.Message)" -ForegroundColor Red
if ($_.Exception.Response) {
$errorResponse = $_.Exception.Response.GetResponseStream()
$reader = New-Object System.IO.StreamReader($errorResponse)
$errorBody = $reader.ReadToEnd()
Write-Host "错误详情: $errorBody" -ForegroundColor Red
}
}
Write-Host "`n=== 调试完成 ===" -ForegroundColor Green
Write-Host "请查看上述输出,重点关注:" -ForegroundColor Yellow
Write-Host "1. TTL值的变化" -ForegroundColor White
Write-Host "2. attempts字段的变化" -ForegroundColor White
Write-Host "3. 验证码是否被意外删除" -ForegroundColor White

View File

@@ -343,4 +343,23 @@ export class LoginController {
async resendEmailVerification(@Body() sendEmailVerificationDto: SendEmailVerificationDto): Promise<ApiResponse<{ verification_code?: string }>> {
return await this.loginService.resendEmailVerification(sendEmailVerificationDto.email);
}
/**
* 调试验证码信息
* 仅用于开发和调试
*
* @param sendEmailVerificationDto 邮箱信息
* @returns 验证码调试信息
*/
@ApiOperation({
summary: '调试验证码信息',
description: '获取验证码的详细调试信息(仅开发环境)'
})
@ApiBody({ type: SendEmailVerificationDto })
@Post('debug-verification-code')
@HttpCode(HttpStatus.OK)
@UsePipes(new ValidationPipe({ transform: true }))
async debugVerificationCode(@Body() sendEmailVerificationDto: SendEmailVerificationDto): Promise<any> {
return await this.loginService.debugVerificationCode(sendEmailVerificationDto.email);
}
}

View File

@@ -427,4 +427,31 @@ export class LoginService {
// 简单的Base64编码实际应用中应使用JWT
return Buffer.from(JSON.stringify(payload)).toString('base64');
}
/**
* 调试验证码信息
*
* @param email 邮箱地址
* @returns 调试信息
*/
async debugVerificationCode(email: string): Promise<any> {
try {
this.logger.log(`调试验证码信息: ${email}`);
const debugInfo = await this.loginCoreService.debugVerificationCode(email);
return {
success: true,
data: debugInfo,
message: '调试信息获取成功'
};
} catch (error) {
this.logger.error(`获取验证码调试信息失败: ${email}`, error instanceof Error ? error.stack : String(error));
return {
success: false,
message: error instanceof Error ? error.message : '获取调试信息失败',
error_code: 'DEBUG_VERIFICATION_CODE_FAILED'
};
}
}
}

View File

@@ -567,4 +567,16 @@ export class LoginCoreService {
const phoneRegex = /^(\+\d{1,3}[- ]?)?\d{10,11}$/;
return phoneRegex.test(str.replace(/\s/g, ''));
}
}
/**
* 调试验证码信息
*
* @param email 邮箱地址
* @returns 调试信息
*/
async debugVerificationCode(email: string): Promise<any> {
return await this.verificationService.debugCodeInfo(
email,
VerificationCodeType.EMAIL_VERIFICATION
);
}
}

View File

@@ -111,24 +111,50 @@ export class VerificationService {
const codeInfoStr = await this.redis.get(key);
if (!codeInfoStr) {
this.logger.warn(`验证码不存在或已过期: ${identifier} (${type})`);
throw new BadRequestException('验证码不存在或已过期');
}
const codeInfo: VerificationCodeInfo = JSON.parse(codeInfoStr);
let codeInfo: VerificationCodeInfo;
try {
codeInfo = JSON.parse(codeInfoStr);
} catch (error) {
this.logger.error(`验证码数据解析失败: ${identifier} (${type})`, error);
await this.redis.del(key);
throw new BadRequestException('验证码数据异常,请重新获取');
}
// 检查尝试次数
if (codeInfo.attempts >= codeInfo.maxAttempts) {
this.logger.warn(`验证码尝试次数已达上限: ${identifier} (${type}), 尝试次数: ${codeInfo.attempts}`);
await this.redis.del(key);
throw new BadRequestException('验证码尝试次数过多,请重新获取');
}
// 增加尝试次数
codeInfo.attempts++;
await this.redis.set(key, JSON.stringify(codeInfo), this.CODE_EXPIRE_TIME);
// 获取当前TTL
const currentTTL = await this.redis.ttl(key);
this.logger.debug(`验证码当前TTL: ${identifier} (${type}), TTL: ${currentTTL}`);
// 验证验证码
if (codeInfo.code !== inputCode) {
this.logger.warn(`验证码验证失败: ${identifier} (${type}) - 剩余尝试次数: ${codeInfo.maxAttempts - codeInfo.attempts}`);
// 增加尝试次数
codeInfo.attempts++;
// 保持原有的TTL不重置过期时间
if (currentTTL > 0) {
// 使用剩余的TTL时间
await this.redis.set(key, JSON.stringify(codeInfo), currentTTL);
this.logger.warn(`验证码验证失败: ${identifier} (${type}) - 剩余尝试次数: ${codeInfo.maxAttempts - codeInfo.attempts}, 剩余时间: ${currentTTL}`);
} else if (currentTTL === -1) {
// 永不过期的情况,保持永不过期
await this.redis.set(key, JSON.stringify(codeInfo));
this.logger.warn(`验证码验证失败: ${identifier} (${type}) - 剩余尝试次数: ${codeInfo.maxAttempts - codeInfo.attempts}, 永不过期`);
} else {
// TTL为-2表示键不存在这种情况理论上不应该发生
this.logger.error(`验证码TTL异常: ${identifier} (${type}), TTL: ${currentTTL}`);
throw new BadRequestException('验证码状态异常,请重新获取');
}
throw new BadRequestException(`验证码错误,剩余尝试次数: ${codeInfo.maxAttempts - codeInfo.attempts}`);
}
@@ -288,13 +314,16 @@ export class VerificationService {
ttl: number;
attempts?: number;
maxAttempts?: number;
code?: string;
createdAt?: 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 };
this.logger.debug(`验证码不存在: ${identifier} (${type})`);
return { exists: false, ttl: -2 };
}
const codeInfoStr = await this.redis.get(key);
@@ -307,11 +336,54 @@ export class VerificationService {
codeInfo = {} as VerificationCodeInfo;
}
this.logger.debug(`验证码统计: ${identifier} (${type}), TTL: ${ttl}, 尝试次数: ${codeInfo.attempts}/${codeInfo.maxAttempts}`);
return {
exists: true,
ttl,
attempts: codeInfo.attempts,
maxAttempts: codeInfo.maxAttempts,
code: codeInfo.code, // 仅用于调试,生产环境应该移除
createdAt: codeInfo.createdAt,
};
}
/**
* 调试方法:获取验证码详细信息
* 仅用于开发和调试,生产环境应该移除或限制访问
*
* @param identifier 标识符
* @param type 验证码类型
* @returns 详细信息
*/
async debugCodeInfo(identifier: string, type: VerificationCodeType): Promise<any> {
const key = this.buildRedisKey(identifier, type);
const [exists, ttl, rawData] = await Promise.all([
this.redis.exists(key),
this.redis.ttl(key),
this.redis.get(key)
]);
const result = {
key,
exists,
ttl,
rawData,
parsedData: null as any,
currentTime: Date.now(),
timeFormatted: new Date().toISOString()
};
if (rawData) {
try {
result.parsedData = JSON.parse(rawData);
} catch (error) {
result.parsedData = { error: 'JSON解析失败', raw: rawData };
}
}
this.logger.debug(`调试验证码信息: ${JSON.stringify(result, null, 2)}`);
return result;
}
}