forked from datawhale/whale-town-end
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:
112
Test-Verification-Debug.ps1
Normal file
112
Test-Verification-Debug.ps1
Normal 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
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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'
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user