feat: implement enhanced captcha system

This commit is contained in:
张翔
2026-03-24 10:36:30 +08:00
parent 7dbaccc4ba
commit cef0b6fb74
2 changed files with 167 additions and 0 deletions
+46
View File
@@ -0,0 +1,46 @@
import { generateCaptcha, validateCaptcha, CaptchaResult } from './captcha';
describe('Enhanced Captcha System', () => {
test('should generate captcha with medium complexity', () => {
const result = generateCaptcha('medium');
expect(result).toBeDefined();
expect(result.question).toBeDefined();
expect(result.answer).toBeDefined();
expect(result.hash).toBeDefined();
expect(result.timestamp).toBeDefined();
expect(typeof result.question).toBe('string');
expect(typeof result.answer).toBe('number');
});
test('should generate different captcha each time', () => {
const result1 = generateCaptcha('medium');
const result2 = generateCaptcha('medium');
expect(result1.hash).not.toBe(result2.hash);
});
test('should validate correct captcha answer', () => {
const captcha = generateCaptcha('medium');
const isValid = validateCaptcha(captcha.hash, captcha.answer, captcha.timestamp);
expect(isValid).toBe(true);
});
test('should reject incorrect captcha answer', () => {
const captcha = generateCaptcha('medium');
const isValid = validateCaptcha(captcha.hash, captcha.answer + 1, captcha.timestamp);
expect(isValid).toBe(false);
});
test('should reject expired captcha', () => {
const captcha = generateCaptcha('medium');
const expiredTimestamp = Date.now() - (6 * 60 * 1000); // 6 minutes ago
const isValid = validateCaptcha(captcha.hash, captcha.answer, expiredTimestamp);
expect(isValid).toBe(false);
});
test('should generate complex captcha', () => {
const result = generateCaptcha('complex');
expect(result).toBeDefined();
const numbers = result.question.match(/\d+/g);
expect(numbers?.length).toBeGreaterThan(2);
});
});
+121
View File
@@ -0,0 +1,121 @@
import { getSecurityConfig } from './config';
export interface CaptchaResult {
question: string;
answer: number;
hash: string;
timestamp: number;
}
function generateSimpleCaptcha(): CaptchaResult {
const num1 = Math.floor(Math.random() * 10) + 1;
const num2 = Math.floor(Math.random() * 10) + 1;
const answer = num1 + num2;
const question = `${num1} + ${num2} = ?`;
return {
question,
answer,
hash: '',
timestamp: Date.now(),
};
}
function generateMediumCaptcha(): CaptchaResult {
const operations = ['+', '-', '*'];
const operation = operations[Math.floor(Math.random() * operations.length)];
const num1 = Math.floor(Math.random() * 20) + 1;
const num2 = Math.floor(Math.random() * 10) + 1;
let answer: number;
let question: string;
switch (operation) {
case '+':
answer = num1 + num2;
question = `${num1} + ${num2} = ?`;
break;
case '-':
answer = num1 - num2;
question = `${num1} - ${num2} = ?`;
break;
case '*':
answer = num1 * num2;
question = `${num1} * ${num2} = ?`;
break;
default:
answer = num1 + num2;
question = `${num1} + ${num2} = ?`;
}
return {
question,
answer,
hash: '',
timestamp: Date.now(),
};
}
function generateComplexCaptcha(): CaptchaResult {
const num1 = Math.floor(Math.random() * 15) + 1;
const num2 = Math.floor(Math.random() * 10) + 1;
const num3 = Math.floor(Math.random() * 5) + 1;
const operations = ['+', '-', '*'];
const op1 = operations[Math.floor(Math.random() * operations.length)];
const op2 = operations[Math.floor(Math.random() * operations.length)];
let answer: number;
let question: string;
const temp1 = op1 === '+' ? num1 + num2 : op1 === '-' ? num1 - num2 : num1 * num2;
answer = op2 === '+' ? temp1 + num3 : op2 === '-' ? temp1 - num3 : temp1 * num3;
question = `(${num1} ${op1} ${num2}) ${op2} ${num3} = ?`;
return {
question,
answer,
hash: '',
timestamp: Date.now(),
};
}
function generateHash(answer: number, timestamp: number): string {
const secret = process.env.CAPTCHA_SECRET || 'default-secret-key';
const data = `${answer}-${timestamp}-${secret}`;
return Buffer.from(data).toString('base64');
}
export function generateCaptcha(complexity: 'simple' | 'medium' | 'complex' = 'medium'): CaptchaResult {
let captcha: CaptchaResult;
switch (complexity) {
case 'simple':
captcha = generateSimpleCaptcha();
break;
case 'complex':
captcha = generateComplexCaptcha();
break;
case 'medium':
default:
captcha = generateMediumCaptcha();
break;
}
captcha.hash = generateHash(captcha.answer, captcha.timestamp);
return captcha;
}
export function validateCaptcha(hash: string, answer: number, timestamp: number): boolean {
const config = getSecurityConfig();
const now = Date.now();
const ageMinutes = (now - timestamp) / (1000 * 60);
if (ageMinutes > config.captcha.expiryMinutes) {
return false;
}
const expectedHash = generateHash(answer, timestamp);
return hash === expectedHash;
}