This commit is contained in:
Baobhan Sith
2025-04-25 10:03:56 +08:00
parent 452922724d
commit 5c2a159792
18 changed files with 995 additions and 66 deletions
@@ -0,0 +1,120 @@
import axios from 'axios';
import { settingsService } from './settings.service';
import { CaptchaSettings, CaptchaProvider } from '../types/settings.types';
// CAPTCHA 验证 API 端点
const HCAPTCHA_VERIFY_URL = 'https://api.hcaptcha.com/siteverify';
const RECAPTCHA_VERIFY_URL = 'https://www.google.com/recaptcha/api/siteverify'; // v2
export class CaptchaService {
/**
* 验证提供的 CAPTCHA 令牌。
* 根据系统设置自动选择合适的提供商进行验证。
* @param token - 从前端获取的 CAPTCHA 令牌 (h-captcha-response 或 g-recaptcha-response)
* @returns Promise<boolean> - 令牌是否有效
* @throws Error 如果配置无效或验证请求失败
*/
async verifyToken(token: string): Promise<boolean> {
if (!token) {
console.warn('[CaptchaService] 验证失败:未提供令牌。');
return false; // 没有令牌,直接视为无效
}
const captchaConfig = await settingsService.getCaptchaConfig();
if (!captchaConfig.enabled) {
console.log('[CaptchaService] CAPTCHA 未启用,跳过验证。');
return true; // 未启用则视为验证通过
}
switch (captchaConfig.provider) {
case 'hcaptcha':
if (!captchaConfig.hcaptchaSecretKey) {
throw new Error('hCaptcha 配置无效:缺少 Secret Key。');
}
return this._verifyHCaptcha(token, captchaConfig.hcaptchaSecretKey);
case 'recaptcha':
if (!captchaConfig.recaptchaSecretKey) {
throw new Error('Google reCAPTCHA 配置无效:缺少 Secret Key。');
}
return this._verifyReCaptcha(token, captchaConfig.recaptchaSecretKey);
case 'none':
console.log('[CaptchaService] CAPTCHA 提供商设置为 "none",跳过验证。');
return true; // 提供商为 none 也视为通过
default:
console.error(`[CaptchaService] 未知的 CAPTCHA 提供商: ${captchaConfig.provider}`);
throw new Error(`未知的 CAPTCHA 提供商配置: ${captchaConfig.provider}`);
}
}
/**
* 调用 hCaptcha API 验证令牌。
* @param token - h-captcha-response 令牌
* @param secretKey - hCaptcha Secret Key
* @returns Promise<boolean> - 令牌是否有效
*/
private async _verifyHCaptcha(token: string, secretKey: string): Promise<boolean> {
console.log('[CaptchaService] 正在验证 hCaptcha 令牌...');
try {
const response = await axios.post(HCAPTCHA_VERIFY_URL, null, { // 使用 POST,数据在 params 中
params: {
secret: secretKey,
response: token,
// remoteip: 可选,用户 IP 地址
},
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
});
console.log('[CaptchaService] hCaptcha 验证响应:', response.data);
if (response.data && response.data.success === true) {
console.log('[CaptchaService] hCaptcha 令牌验证成功。');
return true;
} else {
console.warn('[CaptchaService] hCaptcha 令牌验证失败:', response.data['error-codes'] || '未知错误');
return false;
}
} catch (error: any) {
console.error('[CaptchaService] 调用 hCaptcha 验证 API 时出错:', error.response?.data || error.message);
// 抛出错误,让上层处理(例如,提示用户稍后重试)
throw new Error(`hCaptcha 验证请求失败: ${error.message}`);
}
}
/**
* 调用 Google reCAPTCHA API 验证令牌。
* @param token - g-recaptcha-response 令牌
* @param secretKey - Google reCAPTCHA Secret Key
* @returns Promise<boolean> - 令牌是否有效
*/
private async _verifyReCaptcha(token: string, secretKey: string): Promise<boolean> {
console.log('[CaptchaService] 正在验证 Google reCAPTCHA 令牌...');
try {
const response = await axios.post(RECAPTCHA_VERIFY_URL, null, { // 使用 POST,数据在 params 中
params: {
secret: secretKey,
response: token,
// remoteip: 可选,用户 IP 地址
},
headers: { 'Content-Type': 'application/x-www-form-urlencoded' }
});
console.log('[CaptchaService] Google reCAPTCHA 验证响应:', response.data);
if (response.data && response.data.success === true) {
// 可选:检查 hostname, score (v3), action (v3) 等
console.log('[CaptchaService] Google reCAPTCHA 令牌验证成功。');
return true;
} else {
console.warn('[CaptchaService] Google reCAPTCHA 令牌验证失败:', response.data['error-codes'] || '未知错误');
return false;
}
} catch (error: any) {
console.error('[CaptchaService] 调用 Google reCAPTCHA 验证 API 时出错:', error.response?.data || error.message);
// 抛出错误,让上层处理
throw new Error(`Google reCAPTCHA 验证请求失败: ${error.message}`);
}
}
}
// 导出一个单例供其他服务使用
export const captchaService = new CaptchaService();