From 02f77b756c4c38d0e93df429545eae27fadd317e Mon Sep 17 00:00:00 2001 From: unknown Date: Mon, 13 Jul 2026 19:52:30 +0800 Subject: [PATCH] fix(captcha): close human-verification loop end-to-end Rewrite captcha core around aksrt_captcha_config only, require captcha on login/comment/SMS/phone auth and related gates, wire frontend token collection, and remove dead dual-source security settings plus slider enqueue. --- .../pages/permissions/CommentPermissions.vue | 2 +- .../src/schema/sections/permissions/index.js | 24 +- assets/js/cms-download.js | 7 +- assets/js/core/comments.js | 20 +- assets/js/modal-auth.js | 169 +- assets/js/sign-register.js | 68 +- ...class-aksrt-admin-extension-controller.php | 27 +- ...-user-controller-auth-phone-operations.php | 11 + ...aksrt-admin-schema-extensions-security.php | 24 +- .../class-aksrt-admin-schema-permissions.php | 2 +- .../trait-aksrt-auth-service-part-1a.php | 40 +- .../trait-aksrt-auth-service-part-1b.php | 31 +- inc/classes/class-aksrt-auth-service.php | 7 + inc/classes/class-aksrt-captcha.php | 2116 +++++++---------- inc/classes/class-aksrt-comment-actions.php | 16 +- inc/classes/class-aksrt-download.php | 19 +- inc/classes/class-aksrt-signin.php | 18 +- inc/classes/class-aksrt-sms.php | 13 +- inc/classes/class-aksrt-user.php | 12 +- .../trait-aksrt-scripts-part-1b.php | 12 - .../class-aksrt-settings-capabilities.php | 40 +- 21 files changed, 1163 insertions(+), 1515 deletions(-) diff --git a/assets/admin/src/pages/permissions/CommentPermissions.vue b/assets/admin/src/pages/permissions/CommentPermissions.vue index 7ca00a62..8a35a9ae 100644 --- a/assets/admin/src/pages/permissions/CommentPermissions.vue +++ b/assets/admin/src/pages/permissions/CommentPermissions.vue @@ -120,7 +120,7 @@ async function load() { form.comment_require_login = !!comment.comment_require_login form.comment_allow_reply = comment.comment_allow_reply !== false - status.commentCaptchaEnabled = !!captcha.enabled && !disabledScenarios.includes('comment') + status.commentCaptchaEnabled = !!captcha.enabled && !!captcha.active_provider && !disabledScenarios.includes('comment') status.moderationEnabled = !!moderation.enabled status.rateLimitEnabled = !!moderation.rate_limit_enabled status.keywordRuleCount = splitKeywords(moderation.blocked_keywords).length diff --git a/assets/admin/src/schema/sections/permissions/index.js b/assets/admin/src/schema/sections/permissions/index.js index f00811de..8574a748 100644 --- a/assets/admin/src/schema/sections/permissions/index.js +++ b/assets/admin/src/schema/sections/permissions/index.js @@ -289,29 +289,15 @@ export const permissionsSchemas = { }, /** - * Download permissions + * Download permissions — live UI is DownloadPermissions.vue via cms_features. + * Keep empty schema so SchemaPage cannot mis-write aksrt_settings. */ download: { key: 'permissions.download', title: '下载权限', icon: 'fas fa-download', - description: '控制资源下载的权限和限制。', - defaults: { - download_require_login: false, - download_daily_limit: 0, - download_captcha: false, - }, - groups: [ - { - key: 'basic', - title: '下载设置', - icon: 'fas fa-cog', - fields: [ - { key: 'download_require_login', label: '需登录下载', type: 'switch' }, - { key: 'download_daily_limit', label: '每日下载限制', type: 'number', min: 0, help: '0 表示不限制' }, - { key: 'download_captcha', label: '下载验证码', type: 'switch', help: '下载前需要验证码' }, - ], - }, - ], + description: '下载门槛由 DownloadPermissions 页维护(cms_features);人机验证服务商在登录权限页。', + defaults: {}, + groups: [], }, }; diff --git a/assets/js/cms-download.js b/assets/js/cms-download.js index c2e219f7..eec22a88 100644 --- a/assets/js/cms-download.js +++ b/assets/js/cms-download.js @@ -231,8 +231,11 @@ const state = window.AKSRTCaptchaState; const scene = state && state.scenes && state.scenes.download; if (!scene || typeof scene.verify !== 'function') { - // captcha 组件还未就绪,直接发,由后端兜底报错 - callback(); + // captcha 组件未就绪:不直发,避免绕过服务端门槛 + if (window.console && console.warn) { + console.warn('[AKSRT] download captcha not ready'); + } + alert(tt('captcha_loading', '验证码组件加载中,请稍候再试')); return; } scene.verify(function () { callback(); }); diff --git a/assets/js/core/comments.js b/assets/js/core/comments.js index f4bc019d..940150d3 100644 --- a/assets/js/core/comments.js +++ b/assets/js/core/comments.js @@ -232,9 +232,15 @@ var replyInfo = document.getElementById('comment-reply-info'); if (replyInfo) replyInfo.style.display = 'none'; - // Reset captcha - var captchaImg = form.querySelector('.imagecaptcha img'); - if (captchaImg) captchaImg.click(); + // Reset captcha after successful submit + try { + var captchaScene = window.AKSRTCaptchaState && + window.AKSRTCaptchaState.scenes && + window.AKSRTCaptchaState.scenes.comment; + if (captchaScene && typeof captchaScene.reset === 'function') { + captchaScene.reset(); + } + } catch (e) { /* ignore */ } // Wait time countdown if (json.wait_time && json.wait_time > 0) { @@ -251,6 +257,14 @@ if (json && json.code) { showToast(json.message || '评论提交失败', 'error'); + try { + var failScene = window.AKSRTCaptchaState && + window.AKSRTCaptchaState.scenes && + window.AKSRTCaptchaState.scenes.comment; + if (failScene && typeof failScene.reset === 'function') { + failScene.reset(); + } + } catch (e) { /* ignore */ } if (json.wait_time) startCountdown(json.wait_time); else setLoading(false); return; diff --git a/assets/js/modal-auth.js b/assets/js/modal-auth.js index 38d050fa..f1b7b7a7 100644 --- a/assets/js/modal-auth.js +++ b/assets/js/modal-auth.js @@ -163,9 +163,33 @@ clearMessages(); } + function withCaptcha(scene, form, next) { + var store = window.AKSRTCaptchaState && window.AKSRTCaptchaState.scenes + ? window.AKSRTCaptchaState.scenes + : null; + var instance = store && store[scene] ? store[scene] : null; + if (instance && typeof instance.verify === 'function') { + instance.verify(function () { + next(serializeForm(form)); + }); + return; + } + next(serializeForm(form)); + } + + function resetCaptcha(scene) { + try { + var store = window.AKSRTCaptchaState && window.AKSRTCaptchaState.scenes + ? window.AKSRTCaptchaState.scenes + : null; + if (store && store[scene] && typeof store[scene].reset === 'function') { + store[scene].reset(); + } + } catch (e) { /* ignore */ } + } + function handleLogin(form) { - var payload = serializeForm(form); - if (!payload.log || !payload.pwd) { + if (!form.querySelector('[name="log"]') || !form.querySelector('[name="pwd"]')) { showMessage('login', '请输入用户名和密码', 'error'); return; } @@ -173,76 +197,93 @@ setLoading(form, true); clearMessages(); - request('/auth/modal-login', payload) - .then(function (result) { - if (result.ok && result.data.success) { - onAuthSuccess(result.data); - return; - } - - var message = result.data.message || '登录失败'; - if (result.data.verify_url) { - message += ' 前往验证'; - } - showMessage('login', message, 'error'); - }) - .catch(function () { - showMessage('login', '网络错误,请稍后重试', 'error'); - }) - .finally(function () { + withCaptcha('login', form, function (payload) { + if (!payload.log || !payload.pwd) { setLoading(form, false); - }); + showMessage('login', '请输入用户名和密码', 'error'); + return; + } + + request('/auth/modal-login', payload) + .then(function (result) { + if (result.ok && result.data.success) { + onAuthSuccess(result.data); + return; + } + + resetCaptcha('login'); + var message = result.data.message || '登录失败'; + if (result.data.verify_url) { + message += ' 前往验证'; + } + showMessage('login', message, 'error'); + }) + .catch(function () { + resetCaptcha('login'); + showMessage('login', '网络错误,请稍后重试', 'error'); + }) + .finally(function () { + setLoading(form, false); + }); + }); } function handleRegister(form) { - var payload = serializeForm(form); - if (!payload.user_login || !payload.user_email || !payload.user_pass || !payload.user_pass_confirm) { - showMessage('register', '请完整填写注册信息', 'error'); - return; - } - - if (payload.user_pass !== payload.user_pass_confirm) { - showMessage('register', '两次输入的密码不一致', 'error'); - return; - } - setLoading(form, true); clearMessages(); - request('/auth/modal-register', payload) - .then(function (result) { - if (!result.ok || !result.data.success) { - showMessage('register', result.data.message || '注册失败', 'error'); - return; - } - - if (result.data.user) { - onAuthSuccess(result.data); - return; - } - - if (result.data.verification_required) { - var verifyMessage = result.data.message || '注册成功,请完成邮箱验证'; - if (result.data.verify_url) { - verifyMessage += ' 前往验证'; - } - showMessage('register', verifyMessage, 'success'); - form.reset(); - return; - } - - var loginMessage = result.data.message || '注册成功,请登录'; - switchTab('login'); - prefillLogin(result.data.user_login || result.data.user_email || ''); - showMessage('login', loginMessage, 'success'); - form.reset(); - }) - .catch(function () { - showMessage('register', '网络错误,请稍后重试', 'error'); - }) - .finally(function () { + withCaptcha('register', form, function (payload) { + if (!payload.user_login || !payload.user_email || !payload.user_pass || !payload.user_pass_confirm) { setLoading(form, false); - }); + showMessage('register', '请完整填写注册信息', 'error'); + return; + } + + if (payload.user_pass !== payload.user_pass_confirm) { + setLoading(form, false); + showMessage('register', '两次输入的密码不一致', 'error'); + return; + } + + request('/auth/modal-register', payload) + .then(function (result) { + if (!result.ok || !result.data.success) { + resetCaptcha('register'); + showMessage('register', result.data.message || '注册失败', 'error'); + return; + } + + if (result.data.user) { + onAuthSuccess(result.data); + return; + } + + if (result.data.verification_required) { + var verifyMessage = result.data.message || '注册成功,请完成邮箱验证'; + if (result.data.verify_url) { + verifyMessage += ' 前往验证'; + } + showMessage('register', verifyMessage, 'success'); + form.reset(); + resetCaptcha('register'); + return; + } + + var loginMessage = result.data.message || '注册成功,请登录'; + switchTab('login'); + prefillLogin(result.data.user_login || result.data.user_email || ''); + showMessage('login', loginMessage, 'success'); + form.reset(); + resetCaptcha('register'); + }) + .catch(function () { + resetCaptcha('register'); + showMessage('register', '网络错误,请稍后重试', 'error'); + }) + .finally(function () { + setLoading(form, false); + }); + }); } function serializeForm(form) { diff --git a/assets/js/sign-register.js b/assets/js/sign-register.js index a03e7a9e..14e9323c 100644 --- a/assets/js/sign-register.js +++ b/assets/js/sign-register.js @@ -81,11 +81,12 @@ // 收集人机验证字段 const form = btn.closest('form') const captchaData = collectCaptchaFields(form) + const scene = btn.dataset.aksrtSmsScene || 'register' fetch(REST_BASE + '/auth/send-sms-code', { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ phone, ...captchaData }), + body: JSON.stringify({ phone, scene, ...captchaData }), }) .then(r => r.json().then(data => ({ ok: r.ok, data }))) .then(({ ok, data }) => { @@ -97,6 +98,12 @@ } else { const msg = (data.data && data.data.message) || data.message || '发送失败' showToast(msg, 'error') + try { + const captchaState = window.AKSRTCaptchaState + if (captchaState && captchaState.scenes && captchaState.scenes[scene]) { + captchaState.scenes[scene].reset() + } + } catch (e) { /* ignore */ } const remaining = data.data && data.data.remaining_time if (remaining && remaining > 0) { sessionStorage.setItem(COUNTDOWN_KEY, String(Date.now() + remaining * 1000)) @@ -265,33 +272,46 @@ body = { phone, password } } - fetch(REST_BASE + endpoint, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(body), - }) - .then(r => r.json().then(data => ({ ok: r.ok, data }))) - .then(({ ok, data }) => { - if (ok && data.success) { - showToast('登录成功', 'success') - setTimeout(() => { - window.location.href = data.data?.redirect || '/' - }, 500) - } else { - showToast(data.message || '登录失败', 'error') + const captchaState = window.AKSRTCaptchaState + const doLogin = () => { + const captchaData = collectCaptchaFields(form) + fetch(REST_BASE + endpoint, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ ...body, ...captchaData }), + }) + .then(r => r.json().then(data => ({ ok: r.ok, data }))) + .then(({ ok, data }) => { + if (ok && data.success) { + showToast('登录成功', 'success') + setTimeout(() => { + window.location.href = data.data?.redirect || '/' + }, 500) + } else { + showToast(data.message || '登录失败', 'error') + if (captchaState && captchaState.scenes && captchaState.scenes.login) { + captchaState.scenes.login.reset() + } + if (submitBtn) { + submitBtn.disabled = false + submitBtn.textContent = '登录' + } + } + }) + .catch(() => { + showToast('网络错误', 'error') if (submitBtn) { submitBtn.disabled = false submitBtn.textContent = '登录' } - } - }) - .catch(() => { - showToast('网络错误', 'error') - if (submitBtn) { - submitBtn.disabled = false - submitBtn.textContent = '登录' - } - }) + }) + } + + if (captchaState && captchaState.scenes && captchaState.scenes.login) { + captchaState.scenes.login.verify(doLogin) + } else { + doLogin() + } } /* ================================================================= diff --git a/inc/classes/admin/class-aksrt-admin-extension-controller.php b/inc/classes/admin/class-aksrt-admin-extension-controller.php index 588c8218..e9142c53 100644 --- a/inc/classes/admin/class-aksrt-admin-extension-controller.php +++ b/inc/classes/admin/class-aksrt-admin-extension-controller.php @@ -173,8 +173,12 @@ class AKSRT_Admin_Extension_Controller extends AKSRT_Admin_Controller_Base { $email_gate_enabled = false; } - $captcha_comment_enabled = !empty($settings['captcha_comment_enabled']); - $captcha_download_enabled = !empty($settings['captcha_download_enabled']); + $captcha_summary = function_exists('aksrt_get_captcha_status_summary') + ? aksrt_get_captcha_status_summary() + : ['enabled' => false, 'configured' => false, 'scenes' => []]; + $captcha_comment_enabled = !empty($captcha_summary['scenes']['comment']); + $features = class_exists('AKSRT_Settings') ? AKSRT_Settings::get_cms_features() : []; + $captcha_download_enabled = !empty($features['download_captcha']) && !empty($captcha_summary['scenes']['download']); $risk_switches = [ 'show_unapproved_comments' => !empty($settings['show_unapproved_comments']), @@ -191,7 +195,8 @@ class AKSRT_Admin_Extension_Controller extends AKSRT_Admin_Controller_Base { 'content_audit_configured' => !empty($services['audit']['configured']), 'ip_geo_enabled' => !empty($services['ip_geo']['enabled']), 'ip_geo_configured' => !empty($services['ip_geo']['configured']), - 'captcha_enabled' => $captcha_comment_enabled || $captcha_download_enabled, + 'captcha_enabled' => !empty($captcha_summary['enabled']) && !empty($captcha_summary['configured']), + 'captcha_summary' => $captcha_summary, 'risk_switches' => $risk_switches, 'services' => $services, ], @@ -452,17 +457,19 @@ class AKSRT_Admin_Extension_Controller extends AKSRT_Admin_Controller_Base { $services['wechat'] = ['enabled' => false, 'provider' => '', 'configured' => false, 'available' => false, 'verified' => false, 'transport' => '', 'missing_fields' => []]; } - // Captcha - $settings = AKSRT_Settings::get_all(); - $captcha_en = class_exists('AKSRT_Captcha') && !empty($settings['captcha_enabled']); + // Captcha — 唯一真相源 aksrt_captcha_config + $captcha = function_exists('aksrt_get_captcha_status_summary') + ? aksrt_get_captcha_status_summary() + : ['enabled' => false, 'provider' => '', 'configured' => false, 'scenes' => []]; $services['captcha'] = [ - 'enabled' => $captcha_en, - 'provider' => sanitize_key((string) ($settings['captcha_provider'] ?? '')), - 'configured' => $captcha_en, - 'available' => $captcha_en, + 'enabled' => !empty($captcha['enabled']) && !empty($captcha['configured']), + 'provider' => (string) ($captcha['provider'] ?? ''), + 'configured' => !empty($captcha['configured']), + 'available' => !empty($captcha['configured']), 'verified' => false, 'transport' => 'http_api', 'missing_fields' => [], + 'scenes' => $captcha['scenes'] ?? [], ]; // IP Geo diff --git a/inc/classes/admin/class-aksrt-admin-user-controller-auth-phone-operations.php b/inc/classes/admin/class-aksrt-admin-user-controller-auth-phone-operations.php index 86a5efff..30147474 100644 --- a/inc/classes/admin/class-aksrt-admin-user-controller-auth-phone-operations.php +++ b/inc/classes/admin/class-aksrt-admin-user-controller-auth-phone-operations.php @@ -181,6 +181,17 @@ trait AKSRT_Admin_User_Controller_Auth_Phone_Operations { public function auth_send_sms_code(WP_REST_Request $request): WP_REST_Response { $params = $this->get_json_params($request); $phone = sanitize_text_field((string) ($params['phone'] ?? '')); + $scene = sanitize_key((string) ($params['scene'] ?? 'register')); + if (!array_key_exists($scene, function_exists('aksrt_captcha_scenes') ? aksrt_captcha_scenes() : [])) { + $scene = 'register'; + } + + if (function_exists('aksrt_require_captcha')) { + $captcha_result = aksrt_require_captcha($scene, is_array($params) ? $params : []); + if ($captcha_result !== true) { + return $this->error((string) $captcha_result, 400); + } + } if (!class_exists('AKSRT_SMS')) { return $this->error('短信服务不可用'); diff --git a/inc/classes/admin/schema/class-aksrt-admin-schema-extensions-security.php b/inc/classes/admin/schema/class-aksrt-admin-schema-extensions-security.php index 564481d8..dd03ac18 100644 --- a/inc/classes/admin/schema/class-aksrt-admin-schema-extensions-security.php +++ b/inc/classes/admin/schema/class-aksrt-admin-schema-extensions-security.php @@ -52,28 +52,8 @@ class AKSRT_Admin_Schema_Extensions_Security { ['key' => 'time_format', 'label' => '时间显示格式', 'type' => 'input', 'default' => 'n月j日 H:i'], ['key' => 'remove_more_wp_head', 'label' => '移除WP head多余代码', 'type' => 'switch', 'default' => true], ]], - ['key' => 'verification', 'title' => '验证设置', 'fields' => [ - ['key' => 'verification_comment_s', 'label' => '评论需要验证', 'type' => 'switch', 'default' => false], - ['key' => 'verification_links_s', 'label' => '链接需要验证', 'type' => 'switch', 'default' => true], - ['key' => 'verification_newposts_s', 'label' => '新文章需要验证', 'type' => 'switch', 'default' => false], - ['key' => 'verification_bbspost_s', 'label' => '论坛帖子需要验证', 'type' => 'switch', 'default' => false], - ['key' => 'verification_signin_exclude', 'label' => '登录人机验证排除账号', 'type' => 'input', 'default' => ''], - ['key' => 'user_verification_type', 'label' => '验证类型', 'type' => 'radio', 'default' => 'slider', 'options' => [ - ['label' => '图形验证码', 'value' => 'image'], - ['label' => '滑块验证', 'value' => 'slider'], - ['label' => '腾讯验证码', 'value' => 'tcaptcha'], - ['label' => '极验验证码', 'value' => 'geetest'], - ['label' => '关闭验证', 'value' => 'null'], - ]], - ]], - ['key' => 'captcha', 'title' => '验证码设置', 'fields' => [ - ['key' => 'geetest_enabled', 'label' => '极验验证码', 'type' => 'switch', 'default' => false], - ['key' => 'geetest_id', 'label' => '极验 ID', 'type' => 'input', 'default' => ''], - ['key' => 'geetest_key', 'label' => '极验 Key', 'type' => 'input', 'default' => ''], - ['key' => 'tcaptcha_enabled', 'label' => '腾讯云验证码', 'type' => 'switch', 'default' => false], - ['key' => 'tcaptcha_appid', 'label' => '腾讯云 AppID', 'type' => 'input', 'default' => ''], - ['key' => 'tcaptcha_secret_id', 'label' => '腾讯云 SecretId', 'type' => 'input', 'default' => ''], - ['key' => 'tcaptcha_secret_key', 'label' => '腾讯云 SecretKey', 'type' => 'input', 'default' => ''], + ['key' => 'captcha-note', 'title' => '人机验证', 'fields' => [ + ['key' => 'captcha_redirect_hint', 'label' => '配置入口', 'type' => 'notice', 'alertType' => 'info', 'alertContent' => '人机验证(腾讯云 / 阿里云 / Cloudflare / 极验)请到「功能&权限 → 登录权限」统一配置;本页不再维护验证码字段。'], ]], ['key' => 'rate-limit', 'title' => '频率限制', 'fields' => [ ['key' => 'brush_limit_post_enabled', 'label' => '文章发布限制', 'type' => 'switch', 'default' => false], diff --git a/inc/classes/admin/schema/class-aksrt-admin-schema-permissions.php b/inc/classes/admin/schema/class-aksrt-admin-schema-permissions.php index a4f91159..e011cc20 100644 --- a/inc/classes/admin/schema/class-aksrt-admin-schema-permissions.php +++ b/inc/classes/admin/schema/class-aksrt-admin-schema-permissions.php @@ -137,7 +137,7 @@ class AKSRT_Admin_Schema_Permissions { ['key' => 'download_role_size', 'label' => '角色文件大小限制', 'type' => 'textarea', 'default' => '', 'help' => '格式: role|max_size_mb,每行一条'], ]], ['key' => 'captcha', 'title' => '安全设置', 'fields' => [ - ['key' => 'download_captcha', 'label' => '下载需要验证码', 'type' => 'switch', 'default' => false], + ['key' => 'download_captcha_hint', 'label' => '下载验证码', 'type' => 'notice', 'alertType' => 'info', 'alertContent' => '下载验证码门槛在「功能&权限 → 下载权限」页(cms_features.download_captcha)维护;服务商与场景在「登录权限」。'], ['key' => 'download_ip_limit', 'label' => 'IP下载限制', 'type' => 'number', 'default' => 0, 'min' => 0, 'help' => '同一IP每小时最大下载次数,0为不限制'], ]], ], diff --git a/inc/classes/auth-traits/trait-aksrt-auth-service-part-1a.php b/inc/classes/auth-traits/trait-aksrt-auth-service-part-1a.php index d79bb349..c9bdd3fb 100644 --- a/inc/classes/auth-traits/trait-aksrt-auth-service-part-1a.php +++ b/inc/classes/auth-traits/trait-aksrt-auth-service-part-1a.php @@ -47,12 +47,10 @@ trait AKSRT_Auth_Service_Part_1a { return ['success' => false, 'error' => $password_check]; } - if (function_exists('aksrt_is_captcha_enabled_for') && aksrt_is_captcha_enabled_for('register')) { - if (function_exists('aksrt_verify_captcha')) { - $captcha_result = aksrt_verify_captcha($post); - if ($captcha_result !== true) { - return ['success' => false, 'error' => $captcha_result]; - } + if (function_exists('aksrt_require_captcha')) { + $captcha_result = aksrt_require_captcha('register', $post); + if ($captcha_result !== true) { + return ['success' => false, 'error' => $captcha_result]; } } @@ -154,12 +152,10 @@ trait AKSRT_Auth_Service_Part_1a { return ['success' => false, 'error' => __('安全验证失败,请刷新页面重试', 'aksrt')]; } - if (function_exists('aksrt_is_captcha_enabled_for') && aksrt_is_captcha_enabled_for('bind')) { - if (function_exists('aksrt_verify_captcha')) { - $captcha_result = aksrt_verify_captcha($post); - if ($captcha_result !== true) { - return ['success' => false, 'error' => $captcha_result]; - } + if (function_exists('aksrt_require_captcha')) { + $captcha_result = aksrt_require_captcha('bind', $post); + if ($captcha_result !== true) { + return ['success' => false, 'error' => $captcha_result]; } } @@ -371,12 +367,10 @@ trait AKSRT_Auth_Service_Part_1a { return ['success' => false, 'error' => __('验证入口无效或已过期,请重新发起验证流程。', 'aksrt')]; } - if (function_exists('aksrt_is_captcha_enabled_for') && aksrt_is_captcha_enabled_for('verify_email')) { - if (function_exists('aksrt_verify_captcha')) { - $captcha_result = aksrt_verify_captcha($post); - if ($captcha_result !== true) { - return ['success' => false, 'error' => $captcha_result]; - } + if (function_exists('aksrt_require_captcha')) { + $captcha_result = aksrt_require_captcha('verify_email', $post); + if ($captcha_result !== true) { + return ['success' => false, 'error' => $captcha_result]; } } @@ -417,12 +411,10 @@ trait AKSRT_Auth_Service_Part_1a { return ['success' => false, 'error' => __('该账号已完成邮箱验证,无需重复发送', 'aksrt')]; } - if (function_exists('aksrt_is_captcha_enabled_for') && aksrt_is_captcha_enabled_for('verify_email')) { - if (function_exists('aksrt_verify_captcha')) { - $captcha_result = aksrt_verify_captcha($post); - if ($captcha_result !== true) { - return ['success' => false, 'error' => $captcha_result]; - } + if (function_exists('aksrt_require_captcha')) { + $captcha_result = aksrt_require_captcha('verify_email', $post); + if ($captcha_result !== true) { + return ['success' => false, 'error' => $captcha_result]; } } diff --git a/inc/classes/auth-traits/trait-aksrt-auth-service-part-1b.php b/inc/classes/auth-traits/trait-aksrt-auth-service-part-1b.php index 354b1ea1..d8603e6f 100644 --- a/inc/classes/auth-traits/trait-aksrt-auth-service-part-1b.php +++ b/inc/classes/auth-traits/trait-aksrt-auth-service-part-1b.php @@ -110,12 +110,10 @@ trait AKSRT_Auth_Service_Part_1b { return ['success' => false, 'error' => __('安全验证失败,请刷新页面重试', 'aksrt')]; } - if (function_exists('aksrt_is_captcha_enabled_for') && aksrt_is_captcha_enabled_for('lostpassword')) { - if (function_exists('aksrt_verify_captcha')) { - $captcha_result = aksrt_verify_captcha($post); - if ($captcha_result !== true) { - return ['success' => false, 'error' => $captcha_result]; - } + if (function_exists('aksrt_require_captcha')) { + $captcha_result = aksrt_require_captcha('lostpassword', $post); + if ($captcha_result !== true) { + return ['success' => false, 'error' => $captcha_result]; } } @@ -247,6 +245,13 @@ trait AKSRT_Auth_Service_Part_1b { return ['success' => false, 'error' => __('密码至少 6 位', 'aksrt')]; } + if (function_exists('aksrt_require_captcha')) { + $captcha_result = aksrt_require_captcha('register', $post); + if ($captcha_result !== true) { + return ['success' => false, 'error' => $captcha_result]; + } + } + // 验证 SMS 验证码 if (class_exists('AKSRT_SMS')) { $verify_result = AKSRT_SMS::get_instance()->verify_code($phone, $code); @@ -321,6 +326,13 @@ trait AKSRT_Auth_Service_Part_1b { return ['success' => false, 'error' => __('手机号格式不正确', 'aksrt')]; } + if (function_exists('aksrt_require_captcha')) { + $captcha_result = aksrt_require_captcha('login', $post); + if ($captcha_result !== true) { + return ['success' => false, 'error' => $captcha_result]; + } + } + $user = self::get_user_by_phone($phone); if (!$user) { return ['success' => false, 'error' => __('该手机号未注册', 'aksrt')]; @@ -359,6 +371,13 @@ trait AKSRT_Auth_Service_Part_1b { return ['success' => false, 'error' => __('手机号格式不正确', 'aksrt')]; } + if (function_exists('aksrt_require_captcha')) { + $captcha_result = aksrt_require_captcha('login', $post); + if ($captcha_result !== true) { + return ['success' => false, 'error' => $captcha_result]; + } + } + // 验证 SMS 验证码 if (class_exists('AKSRT_SMS')) { $verify_result = AKSRT_SMS::get_instance()->verify_code($phone, $code); diff --git a/inc/classes/class-aksrt-auth-service.php b/inc/classes/class-aksrt-auth-service.php index f2093573..7e4e2796 100644 --- a/inc/classes/class-aksrt-auth-service.php +++ b/inc/classes/class-aksrt-auth-service.php @@ -368,6 +368,13 @@ final class AKSRT_Auth_Service { } } + if (function_exists('aksrt_require_captcha')) { + $captcha_result = aksrt_require_captcha('login', $post); + if ($captcha_result !== true) { + return ['success' => false, 'error' => $captcha_result]; + } + } + $creds = [ 'user_login' => $resolved_login, 'user_password' => (string) ($post['pwd'] ?? ''), diff --git a/inc/classes/class-aksrt-captcha.php b/inc/classes/class-aksrt-captcha.php index 83735127..a3636287 100644 --- a/inc/classes/class-aksrt-captcha.php +++ b/inc/classes/class-aksrt-captcha.php @@ -1,1333 +1,917 @@ aksrt_is_captcha_enabled_for(scene) + * -> aksrt_render_captcha(scene) -> 客户端 AKSRTCaptchaState + token + * -> 业务入口 aksrt_require_captcha(scene, $payload) -> proceed/fail。 + * + * 仅支持云厂商:tencent / aliyun / cloudflare / geetest。 + * 不含本地图形码 / 本地滑块。 + * + * @package AKSRT */ defined('ABSPATH') || exit; - -if (!function_exists('aksrt_get_captcha_config')): /** - * 获取人机验证完整配置 + * 支持的验证服务商。 */ -function aksrt_get_captcha_config() { - $default = array( - 'enabled' => false, - 'active_provider' => '', - 'mode' => 'always', - 'disabled_scenarios'=> array(), - ); - return get_option('aksrt_captcha_config', $default); +function aksrt_captcha_providers(): array { + return ['tencent', 'aliyun', 'cloudflare', 'geetest']; } -endif; -if (!function_exists('aksrt_captcha_provider_is_configured')): /** - * 当前服务商必填凭据是否齐全(避免空 captcha_id 打到极验 403)。 + * 人机验证场景白名单。 + */ +function aksrt_captcha_scenes(): array { + return [ + 'comment' => __('发表评论', 'aksrt'), + 'login' => __('用户登录', 'aksrt'), + 'register' => __('用户注册', 'aksrt'), + 'bind' => __('绑定第三方', 'aksrt'), + 'lostpassword' => __('找回密码', 'aksrt'), + 'change_password' => __('修改密码', 'aksrt'), + 'change_email' => __('更改邮箱', 'aksrt'), + 'verify_email' => __('邮箱验证', 'aksrt'), + 'download' => __('资源下载', 'aksrt'), + 'signin' => __('每日签到', 'aksrt'), + ]; +} + +/** + * 默认配置结构。 + */ +function aksrt_captcha_default_config(): array { + return [ + 'enabled' => false, + 'active_provider' => '', + 'disabled_scenarios' => [], + 'tencent' => [], + 'aliyun' => [], + 'cloudflare' => [], + 'geetest' => [], + ]; +} + +/** + * 获取人机验证完整配置(规范化,不合并历史脏键)。 * - * @param string $provider - * @param array $pcfg + * @return array + */ +function aksrt_get_captcha_config(): array { + $cfg = get_option('aksrt_captcha_config', []); + if (!is_array($cfg)) { + $cfg = []; + } + + $default = aksrt_captcha_default_config(); + $provider = isset($cfg['active_provider']) ? sanitize_key((string) $cfg['active_provider']) : ''; + if (!in_array($provider, aksrt_captcha_providers(), true)) { + $provider = ''; + } + + $disabled = isset($cfg['disabled_scenarios']) ? (array) $cfg['disabled_scenarios'] : []; + $disabled = array_values(array_filter(array_map('sanitize_key', $disabled), function ($s) { + return array_key_exists($s, aksrt_captcha_scenes()); + })); + + $result = [ + 'enabled' => !empty($cfg['enabled']), + 'active_provider' => $provider, + 'disabled_scenarios' => $disabled, + ]; + foreach (aksrt_captcha_providers() as $p) { + $result[$p] = isset($cfg[$p]) && is_array($cfg[$p]) ? $cfg[$p] : []; + } + + return $result; +} + +/** + * 各服务商必填字段。 + */ +function aksrt_captcha_required_fields(string $provider): array { + $map = [ + 'tencent' => ['secret_id', 'secret_key', 'app_id', 'app_secret_key'], + 'aliyun' => ['region', 'prefix', 'scene_id', 'access_key_id', 'access_key_secret'], + 'cloudflare' => ['site_key', 'secret_key'], + 'geetest' => ['captcha_id', 'captcha_key'], + ]; + return $map[$provider] ?? []; +} + +/** + * 当前服务商必填凭据是否齐全(避免空 captcha_id 打到第三方 403)。 + */ +function aksrt_captcha_provider_is_configured(string $provider, array $pcfg = []): bool { + if (!in_array($provider, aksrt_captcha_providers(), true)) { + return false; + } + $required = aksrt_captcha_required_fields($provider); + if (empty($required)) { + return false; + } + foreach ($required as $field) { + if (!isset($pcfg[$field]) || trim((string) $pcfg[$field]) === '') { + return false; + } + } + return true; +} + +/** + * 指定场景是否启用人机验证。 + * + * @param string $scene 见 aksrt_captcha_scenes() * @return bool */ -function aksrt_captcha_provider_is_configured($provider, $pcfg = array()) { - $provider = sanitize_key((string) $provider); - if ($provider === '') { - return false; - } - if (!is_array($pcfg)) { - $pcfg = array(); - } - $required = aksrt_captcha_required_fields($provider); - if (empty($required)) { - return false; - } - foreach ($required as $field) { - if (!isset($pcfg[$field]) || trim((string) $pcfg[$field]) === '') { - return false; - } - } - return true; +function aksrt_is_captcha_enabled_for($scene): bool { + $scene = sanitize_key((string) $scene); + if (!array_key_exists($scene, aksrt_captcha_scenes())) { + return false; + } + + $cfg = aksrt_get_captcha_config(); + if (empty($cfg['enabled']) || $cfg['active_provider'] === '') { + return false; + } + + $provider = $cfg['active_provider']; + $pcfg = $cfg[$provider] ?? []; + if (!aksrt_captcha_provider_is_configured($provider, $pcfg)) { + return false; + } + + return !in_array($scene, $cfg['disabled_scenarios'], true); } -endif; -if (!function_exists('aksrt_is_captcha_enabled_for')): /** - * 检查指定场景是否启用人机验证 + * 业务入口统一调用:未启用放行;启用则校验。 * - * @param string $scene comment / login / register / lostpassword - * @return bool + * @param string $scene 场景 + * @param array $payload 客户端提交的验证字段 + * @return true|string 成功返回 true,失败返回错误信息字符串 */ -function aksrt_is_captcha_enabled_for($scene) { - $cfg = aksrt_get_captcha_config(); - if (empty($cfg['enabled'])) return false; - if (empty($cfg['active_provider'])) return false; - $provider = (string) $cfg['active_provider']; - $pcfg = isset($cfg[$provider]) && is_array($cfg[$provider]) ? $cfg[$provider] : array(); - // 启用但凭据未配齐时,前台不渲染/不拦截,避免空 captcha_id 请求第三方 403 - if (!aksrt_captcha_provider_is_configured($provider, $pcfg)) { - return false; - } - $disabled = isset($cfg['disabled_scenarios']) ? $cfg['disabled_scenarios'] : array(); - return !in_array($scene, $disabled, true); +function aksrt_require_captcha(string $scene, array $payload = []) { + if (!aksrt_is_captcha_enabled_for($scene)) { + return true; + } + return aksrt_verify_captcha($payload); } -endif; -if (!function_exists('aksrt_is_native_auth_admin_whitelist')): /** - * 判断当前请求是否属于原生认证后台白名单。 - * - * 前台登录/注册/找回密码已经由主题认证路由接管, - * 这里仅保留 wp-login.php 场景作为后台认证白名单, - * 避免影响 WordPress 后台登录、重新认证等原生流程。 - * - * @return bool + * 后台摘要:enabled / provider / configured / 场景命中状态。 */ -function aksrt_is_native_auth_admin_whitelist() { - return isset($GLOBALS['pagenow']) && $GLOBALS['pagenow'] === 'wp-login.php'; +function aksrt_get_captcha_status_summary(): array { + $cfg = aksrt_get_captcha_config(); + $provider = $cfg['active_provider']; + $scenes = []; + foreach (aksrt_captcha_scenes() as $key => $label) { + $scenes[$key] = aksrt_is_captcha_enabled_for($key); + } + return [ + 'enabled' => !empty($cfg['enabled']), + 'provider' => $provider, + 'configured' => $provider !== '' && aksrt_captcha_provider_is_configured($provider, $cfg[$provider] ?? []), + 'scenes' => $scenes, + ]; } -endif; // ============================================================ -// PHP 7.x 兼容辅助函数(替代 PHP 8.0+ 的 match 表达式) +// 客户端真实 IP(穿透 CDN / 代理) // ============================================================ -if (!function_exists('aksrt_captcha_field_names')): +function aksrt_get_client_ip(): string { + $keys = ['HTTP_X_FORWARDED_FOR', 'HTTP_X_REAL_IP', 'HTTP_CLIENT_IP']; + foreach ($keys as $key) { + if (!empty($_SERVER[$key])) { + $parts = explode(',', (string) $_SERVER[$key]); + $ip = trim($parts[0]); + if (filter_var($ip, FILTER_VALIDATE_IP)) { + return $ip; + } + } + } + return isset($_SERVER['REMOTE_ADDR']) ? (string) $_SERVER['REMOTE_ADDR'] : '0.0.0.0'; +} + +// ============================================================ +// 腾讯云 API 3.0 TC3-HMAC-SHA256 签名 +// ============================================================ + +function aksrt_tencent_tc3_sign($secret_id, $secret_key, $service, $timestamp, $date, $action, $version, $payload): array { + $content_type = 'application/json; charset=utf-8'; + $host = 'captcha.tencentcloudapi.com'; + $payload_hash = hash('sha256', $payload); + $canonical_headers = "content-type:{$content_type}\nhost:{$host}\n"; + $signed_headers = 'content-type;host'; + $canonical_request = "POST\n/\n\n{$canonical_headers}\n{$signed_headers}\n{$payload_hash}"; + $credential_scope = "{$date}/{$service}/tc3_request"; + $string_to_sign = "TC3-HMAC-SHA256\n{$timestamp}\n{$credential_scope}\n" . hash('sha256', $canonical_request); + + $secret_date = hash_hmac('sha256', $date, 'TC3' . $secret_key, true); + $secret_service = hash_hmac('sha256', $service, $secret_date, true); + $secret_signing = hash_hmac('sha256', 'tc3_request', $secret_service, true); + $signature = hash_hmac('sha256', $string_to_sign, $secret_signing); + + $authorization = 'TC3-HMAC-SHA256 ' + . "Credential={$secret_id}/{$credential_scope}, " + . "SignedHeaders={$signed_headers}, " + . "Signature={$signature}"; + + return [ + 'Authorization' => $authorization, + 'Content-Type' => $content_type, + 'Host' => $host, + 'X-TC-Action' => $action, + 'X-TC-Timestamp'=> (string) $timestamp, + 'X-TC-Version' => $version, + 'X-TC-Region' => '', + ]; +} + /** - * 获取指定验证服务商的字段名列表(替代 match) - * - * @param string $provider 服务商标识 - * @return array 字段名数组 + * 阿里云 OpenAPI POP Signature v1.0 (HMAC-SHA1)。 */ -function aksrt_captcha_field_names($provider) { - $map = array( - 'tencent' => array('secret_id', 'secret_key', 'app_id', 'app_secret_key'), - 'aliyun' => array('region', 'prefix', 'scene_id', 'access_key_id', 'access_key_secret'), - 'cloudflare' => array('site_key', 'secret_key', 'domain', 'theme', 'size'), - 'geetest' => array('captcha_id', 'captcha_key', 'product'), - ); - return isset($map[$provider]) ? $map[$provider] : array(); +function aksrt_aliyun_afs_sign(array $params, string $secret): string { + ksort($params); + $canonicalized = ''; + foreach ($params as $k => $v) { + $canonicalized .= '&' . rawurlencode((string) $k) . '=' . rawurlencode((string) $v); + } + $string_to_sign = 'POST&%2F&' . rawurlencode(substr($canonicalized, 1)); + return base64_encode(hash_hmac('sha1', $string_to_sign, $secret . '&', true)); } -endif; -if (!function_exists('aksrt_captcha_required_fields')): +// ============================================================ +// 服务端二次校验 +// ============================================================ + /** - * 获取指定验证服务商的必填字段列表(替代 match) + * 校验客户端提交的人机验证字段。 * - * @param string $provider 服务商标识 - * @return array 必填字段名数组 + * @param array $post_data 客户端字段(captcha_verified + 厂商 token) + * @return true|string 成功返回 true,失败返回错误信息 */ -function aksrt_captcha_required_fields($provider) { - $map = array( - 'tencent' => array('secret_id', 'secret_key', 'app_id', 'app_secret_key'), - 'aliyun' => array('region', 'prefix', 'scene_id', 'access_key_id', 'access_key_secret'), - 'cloudflare' => array('site_key', 'secret_key'), - 'geetest' => array('captcha_id', 'captcha_key'), - ); - return isset($map[$provider]) ? $map[$provider] : array(); -} -endif; - -// ============================================================ -// 人机验证:获取客户端真实IP(穿透CDN/代理) -// ============================================================ - -if (!function_exists('aksrt_get_client_ip')) : -function aksrt_get_client_ip() { - // 按优先级检查代理头 - $keys = array('HTTP_X_FORWARDED_FOR', 'HTTP_X_REAL_IP', 'HTTP_CLIENT_IP'); - foreach ($keys as $key) { - if (!empty($_SERVER[$key])) { - $parts = explode(',', $_SERVER[$key]); - $ip = trim($parts[0]); - if (filter_var($ip, FILTER_VALIDATE_IP)) return $ip; - } - } - return isset($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : '0.0.0.0'; -} -endif; - -// ============================================================ -// 腾讯云 API 3.0 TC3-HMAC-SHA256 签名函数 -// ============================================================ - -if (!function_exists('aksrt_tencent_tc3_sign')) : -function aksrt_tencent_tc3_sign($secret_id, $secret_key, $service, $timestamp, $date, $action, $version, $payload) { - // 1. 拼接签名原文 - $http_request_method = 'POST'; - $canonical_uri = '/'; - $canonical_querystring = ''; - $content_type = 'application/json; charset=utf-8'; - $host = 'captcha.tencentcloudapi.com'; - - $payload_hash = hash('sha256', $payload); - - $canonical_headers = "content-type:{$content_type}\nhost:{$host}\n"; - $signed_headers = 'content-type;host'; - - $canonical_request = "{$http_request_method}\n{$canonical_uri}\n{$canonical_querystring}\n{$canonical_headers}\n{$signed_headers}\n{$payload_hash}"; - - $algorithm = 'TC3-HMAC-SHA256'; - $credential_scope = "{$date}/{$service}/tc3_request"; - $string_to_sign = "{$algorithm}\n{$timestamp}\n{$credential_scope}\n" . hash('sha256', $canonical_request); - - // 2. 计算签名密钥链 - $secret_date = hash_hmac('sha256', $date, 'TC3' . $secret_key, true); - $secret_service = hash_hmac('sha256', $service, $secret_date, true); - $secret_signing = hash_hmac('sha256', 'tc3_request', $secret_service, true); - - // 3. 计算签名 - $signature = hash_hmac('sha256', $string_to_sign, $secret_signing); - - // 4. 组装 Authorization Header(包含 SecretId) - $authorization = "{$algorithm} " - . "Credential={$secret_id}/{$credential_scope}, " - . "SignedHeaders={$signed_headers}, " - . "Signature={$signature}"; - - return array( - 'Authorization' => $authorization, - 'Content-Type' => $content_type, - 'Host' => $host, - 'X-TC-Action' => $action, - 'X-TC-Timestamp'=> (string) $timestamp, - 'X-TC-Version' => $version, - 'X-TC-Region' => '', - ); -} - - - function aksrt_verify_captcha($post_data) { - $cfg = aksrt_get_captcha_config(); - if (empty($cfg['enabled']) || empty($cfg['active_provider'])) { - return true; - } + $cfg = aksrt_get_captcha_config(); + if (empty($cfg['enabled']) || $cfg['active_provider'] === '') { + return true; + } - $provider = $cfg['active_provider']; - $pcfg = isset($cfg[$provider]) && is_array($cfg[$provider]) ? $cfg[$provider] : array(); - $client_ip = aksrt_get_client_ip(); + $provider = $cfg['active_provider']; + $pcfg = $cfg[$provider] ?? []; + $client_ip = aksrt_get_client_ip(); - // 快速路径:客户端验证字段检查 - $captcha_verified = isset($post_data['captcha_verified']) ? $post_data['captcha_verified'] : ''; - if ($captcha_verified !== '1') { - return __('人机验证失败', 'aksrt'); - } + // 客户端验证字段(前端写入 captcha_verified=1 表示用户已完成交互) + if (($post_data['captcha_verified'] ?? '') !== '1') { + return __('人机验证失败', 'aksrt'); + } - $result = false; - $error_msg = ''; + switch ($provider) { + case 'tencent': + $result = aksrt_verify_tencent($pcfg, $post_data, $client_ip); + break; + case 'cloudflare': + $result = aksrt_verify_cloudflare($pcfg, $post_data, $client_ip); + break; + case 'geetest': + $result = aksrt_verify_geetest($pcfg, $post_data); + break; + case 'aliyun': + $result = aksrt_verify_aliyun($pcfg, $post_data); + break; + default: + return true; + } - switch ($provider) { - - // --- 腾讯云验证码 2.0 (API 3.0 DescribeCaptchaResult + TC3-HMAC-SHA256 签名) --- - case 'tencent': - $ticket = isset($post_data['tencent_ticket']) ? $post_data['tencent_ticket'] : ''; - $randstr = isset($post_data['tencent_randstr']) ? $post_data['tencent_randstr'] : ''; - if (!$ticket || !$randstr) { - $error_msg = '缺少验证参数'; - break; - } - - // 检测容灾票据 (trerror_ 前缀表示 SDK 加载失败或异常) - if (strpos($ticket, 'trerror_') === 0) { - $error_msg = '验证码SDK异常,请刷新页面重试 (容灾票据)'; - break; - } - - // 检查是否配置了云 API 密钥(TC3 签名必须) - $cloud_secret_id = isset($pcfg['secret_id']) ? $pcfg['secret_id'] : ''; - $cloud_secret_key = isset($pcfg['secret_key']) ? $pcfg['secret_key'] : ''; - if (!$cloud_secret_id || !$cloud_secret_key) { - $error_msg = '验证码配置不完整:缺少云 API 密钥 (SecretId/SecretKey)'; - break; - } - - // API 3.0 DescribeCaptchaResult 请求参数 - $tc_action = 'DescribeCaptchaResult'; - $tc_version = '2019-07-22'; - $tc_service = 'captcha'; - $tc_host = 'captcha.tencentcloudapi.com'; - $tc_timestamp = time(); - $tc_date = gmdate('Y-m-d', $tc_timestamp); - - $tc_payload = wp_json_encode(array( - 'CaptchaType' => 9, - 'CaptchaAppId' => (int) (isset($pcfg['app_id']) ? $pcfg['app_id'] : 0), - 'AppSecretKey' => isset($pcfg['app_secret_key']) ? $pcfg['app_secret_key'] : '', - 'Ticket' => $ticket, - 'Randstr' => $randstr, - 'UserIp' => $client_ip, - )); - - // 生成 TC3-HMAC-SHA256 签名(使用云 API 密钥) - $headers = aksrt_tencent_tc3_sign( - $cloud_secret_id, - $cloud_secret_key, - $tc_service, - $tc_timestamp, - $tc_date, - $tc_action, - $tc_version, - $tc_payload - ); - - // 使用 cURL 发起签名请求(避免 WordPress HTTP API 干扰 headers) - $ch = curl_init("https://{$tc_host}/"); - $curl_headers = array(); - foreach ($headers as $k => $v) { - $curl_headers[] = "{$k}: {$v}"; - } - curl_setopt_array($ch, array( - CURLOPT_POST => true, - CURLOPT_RETURNTRANSFER => true, - CURLOPT_TIMEOUT => 10, - CURLOPT_POSTFIELDS => $tc_payload, - CURLOPT_HTTPHEADER => $curl_headers, - CURLOPT_SSL_VERIFYPEER => false, - CURLOPT_FOLLOWLOCATION => false, - )); - $response_body = curl_exec($ch); - $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE); - $curl_error = curl_error($ch); - curl_close($ch); - - if ($curl_error) { - $error_msg = '验证码服务请求失败: ' . $curl_error; - break; - } - - $body = json_decode(!empty($response_body) ? $response_body : '{}', true); - - // 检查 API 错误 - if (isset($body['Response']['Error'])) { - $api_err = $body['Response']['Error']; - $err_code = isset($api_err['Code']) ? $api_err['Code'] : ''; - $err_msg = isset($api_err['Message']) ? $api_err['Message'] : ''; - $error_msg = '腾讯 API 调用失败: ' . (!empty($err_msg) ? $err_msg : (!empty($err_code) ? $err_code : '未知错误')); - break; - } - - $resp = isset($body['Response']) && is_array($body['Response']) ? $body['Response'] : array(); - $captcha_code = (int) (isset($resp['CaptchaCode']) ? $resp['CaptchaCode'] : 0); - - // CaptchaCode: 1=验证通过, 其他=失败 - if ($captcha_code === 1) { - $result = true; - } else { - $last_code = $captcha_code; - $last_msg = isset($resp['CaptchaMsg']) ? $resp['CaptchaMsg'] : ''; - - $messages = array( - 0 => '验证未通过', - 7 => 'Randstr不合法', - 8 => 'Ticket已过期(有效期5分钟)', - 9 => 'Ticket被重复使用', - 15 => 'Ticket不合法(解密失败)', - 16 => 'CaptchaAppId与Ticket不匹配', - 21 => '票据校验异常(风控拦截)', - 100 => '参数校验失败(AppId/SecretKey/Ticket不匹配)', - ); - $msg_text = !empty($last_msg) ? $last_msg : (isset($messages[$last_code]) ? $messages[$last_code] : '未知错误'); - $error_msg = '腾讯验证失败: ' . $msg_text . " (CaptchaCode: {$last_code})"; - } - break; - - // --- Cloudflare Turnstile --- - case 'cloudflare': - $token = isset($post_data['cf_turnstile_response']) ? $post_data['cf_turnstile_response'] : ''; - if (!$token) { - $error_msg = '缺少验证参数'; - break; - } - - // Token 格式检查 (Turnstile token 通常是长字符串) - if (strlen($token) < 10) { - $error_msg = '验证码 token 格式异常'; - break; - } - - // 尝试方式1: 不带 remoteip(Cloudflare 自动使用服务器 IP) - // 如果失败再尝试方式2: 带 remoteip - $cf_attempts = array( - 'without-remoteip' => array( - 'body' => array( - 'secret' => isset($pcfg['secret_key']) ? $pcfg['secret_key'] : '', - 'response' => $token, - ) - ), - 'with-remoteip' => array( - 'body' => array( - 'secret' => isset($pcfg['secret_key']) ? $pcfg['secret_key'] : '', - 'response' => $token, - 'remoteip' => $client_ip, - ) - ), - ); - - foreach ($cf_attempts as $mode => $attempt) { - $resp = wp_remote_post('https://challenges.cloudflare.com/turnstile/v0/siteverify', array( - 'timeout' => 10, - 'body' => $attempt['body'], - )); - - if (is_wp_error($resp)) { - continue; - } - - $raw_body = wp_remote_retrieve_body($resp); - $body = json_decode($raw_body, true); - - if (!empty($body['success'])) { - $result = true; - break; - } - } - - if (!$result) { - $error_codes = isset($body['error-codes']) && is_array($body['error-codes']) ? $body['error-codes'] : array(); - $error_codes_str = implode(', ', $error_codes); - $error_msg = 'Cloudflare验证失败: ' . (!empty($error_codes_str) ? $error_codes_str : '未知错误'); - } - break; - - // --- 极验 GeeTest v4 --- - case 'geetest': - $lot_number = isset($post_data['geetest_lot_number']) ? $post_data['geetest_lot_number'] : ''; - $captcha_output = isset($post_data['geetest_captcha_output']) ? $post_data['geetest_captcha_output'] : ''; - $pass_token = isset($post_data['geetest_pass_token']) ? $post_data['geetest_pass_token'] : ''; - $gen_time = isset($post_data['geetest_gen_time']) ? $post_data['geetest_gen_time'] : ''; - if (!$lot_number || !$captcha_output || !$pass_token) { - $error_msg = '缺少验证参数'; - break; - } - - // 极验 v4 要求使用 HMAC-SHA256 签名(非普通 SHA256) - $captcha_key_val = isset($pcfg['captcha_key']) ? $pcfg['captcha_key'] : ''; - $sign_token = hash_hmac('sha256', $lot_number, $captcha_key_val); - - $resp = wp_remote_post('https://gcaptcha4.geetest.com/validate', array( - 'timeout' => 10, - 'body' => array( - 'captcha_id' => isset($pcfg['captcha_id']) ? $pcfg['captcha_id'] : '', - 'lot_number' => $lot_number, - 'captcha_output' => $captcha_output, - 'pass_token' => $pass_token, - 'gen_time' => $gen_time, - 'sign_token' => $sign_token, - ), - )); - - if (is_wp_error($resp)) { - $error_msg = '验证请求失败: ' . $resp->get_error_message(); - break; - } - - $raw_body = wp_remote_retrieve_body($resp); - $body = json_decode($raw_body, true); - - if (!empty($body['result']) && $body['result'] === 'success') { - $result = true; - } else { - $reason = isset($body['reason']) ? $body['reason'] : ''; - $bmsg = isset($body['msg']) ? $body['msg'] : ''; - $error_msg = '极验验证失败: ' . (!empty($reason) ? $reason : (!empty($bmsg) ? $bmsg : '未知错误')); - } - break; - - // --- 阿里云 CAPTCHA 2.0 V3 (VerifyIntelligentCaptcha) --- - case 'aliyun': - $captcha_verify_param = isset($post_data['aliyun_captcha_verify_param']) ? $post_data['aliyun_captcha_verify_param'] : ''; - if (!$captcha_verify_param) { - $error_msg = '缺少验证参数 (captchaVerifyParam)'; - break; - } - - // 确定地域对应的 API 端点 - $region = isset($pcfg['region']) ? $pcfg['region'] : 'cn'; - $endpoint = $region === 'sgp' - ? 'captcha.ap-southeast-1.aliyuncs.com' - : 'captcha.cn-shanghai.aliyuncs.com'; - - $params = array( - 'AccessKeyId' => isset($pcfg['access_key_id']) ? $pcfg['access_key_id'] : '', - 'Action' => 'VerifyIntelligentCaptcha', - 'Format' => 'JSON', - 'SignatureMethod' => 'HMAC-SHA1', - 'SignatureVersion' => '1.0', - 'SignatureNonce' => uniqid('', true), - 'Timestamp' => gmdate('Y-m-d\TH:i:s\Z'), - 'Version' => '2023-03-05', - 'CaptchaVerifyParam' => $captcha_verify_param, - ); - - // 可选: 传入 SceneId 校验一致性(防止前端篡改) - if (!empty($pcfg['scene_id'])) { - $params['SceneId'] = $pcfg['scene_id']; - } - - $ak_secret = isset($pcfg['access_key_secret']) ? $pcfg['access_key_secret'] : ''; - $params['Signature'] = aksrt_aliyun_afs_sign($params, $ak_secret); - - $resp = wp_remote_post('https://' . $endpoint . '/', array( - 'timeout' => 10, - 'body' => $params, - )); - - if (is_wp_error($resp)) { - $error_msg = '验证请求失败: ' . $resp->get_error_message(); - break; - } - - $body = json_decode(wp_remote_retrieve_body($resp), true); - - if (!empty($body['Success'])) { - $verify_result = isset($body['Result']['VerifyResult']) ? $body['Result']['VerifyResult'] : false; - $verify_code = isset($body['Result']['VerifyCode']) ? $body['Result']['VerifyCode'] : ''; - - if ($verify_result === true) { - $result = true; - } else { - // 根据 VerifyCode 映射错误信息 - $code_messages = array( - 'F001' => '疑似攻击请求,风险策略不通过', - 'F002' => '验证参数为空', - 'F003' => '验证参数格式不合法', - 'F004' => '测试模式配置为不通过', - 'F005' => '场景ID不合法', - 'F006' => '场景ID与配置不匹配', - 'F008' => '验证数据重复提交', - 'F009' => '检测到虚拟设备环境', - 'F010' => '同IP访问频率超限', - 'F011' => '同设备访问频率超限', - 'F012' => 'SceneID与前端配置不一致', - 'F014' => '无初始化记录或已过期(20分钟)', - 'F015' => '验证交互不通过', - 'F018' => 'CaptchaVerifyParam重复使用', - 'F019' => '验证请求超时(90秒)', - 'F020' => 'CaptchaVerifyParam与场景ID不匹配', - ); - $vc_display = !empty($verify_code) ? $verify_code : 'N/A'; - $code_msg = isset($code_messages[$verify_code]) ? $code_messages[$verify_code] : ('未知错误 (' . $vc_display . ')'); - $error_msg = '阿里云V3验证失败: ' . $code_msg; - } - } else { - $amsg = isset($body['Message']) ? $body['Message'] : ''; - $error_msg = '阿里云V3验证失败: ' . (!empty($amsg) ? $amsg : '未知错误'); - } - break; - - default: - return true; - } - - if ($result) { - return true; - } - - return __('人机验证失败', 'aksrt'); + return $result === true ? true : ($result ?: __('人机验证失败', 'aksrt')); } -endif; /** - * 阿里云 OpenAPI 签名函数 (POP Signature v1.0 / HMAC-SHA1) - * 同时适用于 CAPTCHA 2.0 V3 (VerifyIntelligentCaptcha) 和旧版 AFS + * 腾讯云 Captcha 2.0(DescribeCaptchaResult + TC3-HMAC-SHA256)。 + * + * @return true|string */ -if (!function_exists('aksrt_aliyun_afs_sign')) : -function aksrt_aliyun_afs_sign($params, $secret) { - ksort($params); - $canonicalized = ''; - foreach ($params as $k => $v) { - $canonicalized .= '&' . rawurlencode($k) . '=' . rawurlencode($v); - } - $string_to_sign = 'POST&%2F&' . rawurlencode(substr($canonicalized, 1)); - $signature = base64_encode(hash_hmac('sha1', $string_to_sign, $secret . '&', true)); - return $signature; +function aksrt_verify_tencent(array $pcfg, array $post_data, string $client_ip) { + $ticket = (string) ($post_data['tencent_ticket'] ?? ''); + $randstr = (string) ($post_data['tencent_randstr'] ?? ''); + if ($ticket === '' || $randstr === '') { + return __('缺少验证参数', 'aksrt'); + } + if (str_starts_with($ticket, 'trerror_')) { + return __('验证码 SDK 异常,请刷新页面重试(容灾票据)', 'aksrt'); + } + + $secret_id = (string) ($pcfg['secret_id'] ?? ''); + $secret_key = (string) ($pcfg['secret_key'] ?? ''); + if ($secret_id === '' || $secret_key === '') { + return __('验证码配置不完整:缺少云 API 密钥 (SecretId/SecretKey)', 'aksrt'); + } + + $timestamp = time(); + $date = gmdate('Y-m-d', $timestamp); + $payload = wp_json_encode([ + 'CaptchaType' => 9, + 'CaptchaAppId' => (int) ($pcfg['app_id'] ?? 0), + 'AppSecretKey' => (string) ($pcfg['app_secret_key'] ?? ''), + 'Ticket' => $ticket, + 'Randstr' => $randstr, + 'UserIp' => $client_ip, + ]); + + $headers = aksrt_tencent_tc3_sign($secret_id, $secret_key, 'captcha', $timestamp, $date, 'DescribeCaptchaResult', '2019-07-22', $payload); + + $curl_headers = []; + foreach ($headers as $k => $v) { + $curl_headers[] = "{$k}: {$v}"; + } + + $ch = curl_init('https://captcha.tencentcloudapi.com/'); + curl_setopt_array($ch, [ + CURLOPT_POST => true, + CURLOPT_RETURNTRANSFER => true, + CURLOPT_TIMEOUT => 10, + CURLOPT_POSTFIELDS => $payload, + CURLOPT_HTTPHEADER => $curl_headers, + CURLOPT_SSL_VERIFYPEER => false, + CURLOPT_FOLLOWLOCATION => false, + ]); + $response_body = curl_exec($ch); + $curl_error = curl_error($ch); + curl_close($ch); + + if ($curl_error) { + return '验证码服务请求失败: ' . $curl_error; + } + + $body = json_decode((string) $response_body, true); + if (isset($body['Response']['Error'])) { + $err = $body['Response']['Error']; + return '腾讯 API 调用失败: ' . ($err['Message'] ?? $err['Code'] ?? '未知错误'); + } + + $resp = $body['Response'] ?? []; + $captcha_code = (int) ($resp['CaptchaCode'] ?? 0); + if ($captcha_code === 1) { + return true; + } + + $messages = [ + 0 => '验证未通过', + 7 => 'Randstr 不合法', + 8 => 'Ticket 已过期(有效期 5 分钟)', + 9 => 'Ticket 被重复使用', + 15 => 'Ticket 不合法(解密失败)', + 16 => 'CaptchaAppId 与 Ticket 不匹配', + 21 => '票据校验异常(风控拦截)', + 100 => '参数校验失败(AppId/SecretKey/Ticket 不匹配)', + ]; + $msg = $resp['CaptchaMsg'] ?? ($messages[$captcha_code] ?? '未知错误'); + return '腾讯验证失败: ' . $msg . " (CaptchaCode: {$captcha_code})"; } +/** + * Cloudflare Turnstile siteverify。 + * + * @return true|string + */ +function aksrt_verify_cloudflare(array $pcfg, array $post_data, string $client_ip) { + $token = (string) ($post_data['cf_turnstile_response'] ?? ''); + if ($token === '' || strlen($token) < 10) { + return __('缺少验证参数', 'aksrt'); + } + $secret = (string) ($pcfg['secret_key'] ?? ''); + $bodies = [ + ['secret' => $secret, 'response' => $token], + ['secret' => $secret, 'response' => $token, 'remoteip' => $client_ip], + ]; + $body = []; + foreach ($bodies as $attempt) { + $resp = wp_remote_post('https://challenges.cloudflare.com/turnstile/v0/siteverify', [ + 'timeout' => 10, + 'body' => $attempt, + ]); + if (is_wp_error($resp)) { + continue; + } + $body = json_decode(wp_remote_retrieve_body($resp), true); + if (!empty($body['success'])) { + return true; + } + } + + $codes = isset($body['error-codes']) && is_array($body['error-codes']) ? implode(', ', $body['error-codes']) : '未知错误'; + return 'Cloudflare 验证失败: ' . $codes; +} + +/** + * 极验 GeeTest v4 validate。 + * + * @return true|string + */ +function aksrt_verify_geetest(array $pcfg, array $post_data) { + $lot_number = (string) ($post_data['geetest_lot_number'] ?? ''); + $captcha_output = (string) ($post_data['geetest_captcha_output'] ?? ''); + $pass_token = (string) ($post_data['geetest_pass_token'] ?? ''); + $gen_time = (string) ($post_data['geetest_gen_time'] ?? ''); + if ($lot_number === '' || $captcha_output === '' || $pass_token === '') { + return __('缺少验证参数', 'aksrt'); + } + + $captcha_key = (string) ($pcfg['captcha_key'] ?? ''); + $sign_token = hash_hmac('sha256', $lot_number, $captcha_key); + + $resp = wp_remote_post('https://gcaptcha4.geetest.com/validate', [ + 'timeout' => 10, + 'body' => [ + 'captcha_id' => (string) ($pcfg['captcha_id'] ?? ''), + 'lot_number' => $lot_number, + 'captcha_output' => $captcha_output, + 'pass_token' => $pass_token, + 'gen_time' => $gen_time, + 'sign_token' => $sign_token, + ], + ]); + + if (is_wp_error($resp)) { + return '验证请求失败: ' . $resp->get_error_message(); + } + + $body = json_decode(wp_remote_retrieve_body($resp), true); + if (!empty($body['result']) && $body['result'] === 'success') { + return true; + } + $reason = $body['reason'] ?? ($body['msg'] ?? '未知错误'); + return '极验验证失败: ' . $reason; +} + +/** + * 阿里云 CAPTCHA 2.0 V3 (VerifyIntelligentCaptcha)。 + * + * @return true|string + */ +function aksrt_verify_aliyun(array $pcfg, array $post_data) { + $captcha_verify_param = (string) ($post_data['aliyun_captcha_verify_param'] ?? ''); + if ($captcha_verify_param === '') { + return __('缺少验证参数 (captchaVerifyParam)', 'aksrt'); + } + + $region = (string) ($pcfg['region'] ?? 'cn'); + $endpoint = $region === 'sgp' + ? 'captcha.ap-southeast-1.aliyuncs.com' + : 'captcha.cn-shanghai.aliyuncs.com'; + + $params = [ + 'AccessKeyId' => (string) ($pcfg['access_key_id'] ?? ''), + 'Action' => 'VerifyIntelligentCaptcha', + 'Format' => 'JSON', + 'SignatureMethod' => 'HMAC-SHA1', + 'SignatureVersion' => '1.0', + 'SignatureNonce' => uniqid('', true), + 'Timestamp' => gmdate('Y-m-d\TH:i:s\Z'), + 'Version' => '2023-03-05', + 'CaptchaVerifyParam' => $captcha_verify_param, + ]; + if (!empty($pcfg['scene_id'])) { + $params['SceneId'] = (string) $pcfg['scene_id']; + } + $params['Signature'] = aksrt_aliyun_afs_sign($params, (string) ($pcfg['access_key_secret'] ?? '')); + + $resp = wp_remote_post('https://' . $endpoint . '/', [ + 'timeout' => 10, + 'body' => $params, + ]); + + if (is_wp_error($resp)) { + return '验证请求失败: ' . $resp->get_error_message(); + } + + $body = json_decode(wp_remote_retrieve_body($resp), true); + if (!empty($body['Success'])) { + $verify_result = $body['Result']['VerifyResult'] ?? false; + if ($verify_result === true) { + return true; + } + $code_messages = [ + 'F001' => '疑似攻击请求,风险策略不通过', + 'F002' => '验证参数为空', + 'F003' => '验证参数格式不合法', + 'F004' => '测试模式配置为不通过', + 'F005' => '场景 ID 不合法', + 'F006' => '场景 ID 与配置不匹配', + 'F008' => '验证数据重复提交', + 'F009' => '检测到虚拟设备环境', + 'F010' => '同 IP 访问频率超限', + 'F011' => '同设备访问频率超限', + 'F012' => 'SceneID 与前端配置不一致', + 'F014' => '无初始化记录或已过期(20 分钟)', + 'F015' => '验证交互不通过', + 'F018' => 'CaptchaVerifyParam 重复使用', + 'F019' => '验证请求超时(90 秒)', + 'F020' => 'CaptchaVerifyParam 与场景 ID 不匹配', + ]; + $verify_code = (string) ($body['Result']['VerifyCode'] ?? ''); + return '阿里云 V3 验证失败: ' . ($code_messages[$verify_code] ?? '未知错误 (' . ($verify_code ?: 'N/A') . ')'); + } + + return '阿里云 V3 验证失败: ' . ($body['Message'] ?? '未知错误'); +} + +// ============================================================ +// 前端渲染(挂载点 + 内联 SDK + AKSRTCaptchaState) +// ============================================================ + +/** + * 渲染人机验证挂载点。未启用场景直接返回。 + */ function aksrt_render_captcha($scene = 'comment') { - $cfg = aksrt_get_captcha_config(); - if (empty($cfg['enabled']) || empty($cfg['active_provider'])) return; - if (!aksrt_is_captcha_enabled_for($scene)) return; - $provider = $cfg['active_provider']; - $pcfg = isset($cfg[$provider]) && is_array($cfg[$provider]) ? $cfg[$provider] : array(); - if (!aksrt_captcha_provider_is_configured($provider, $pcfg)) { - if (current_user_can('manage_options')) { - echo '
'; - echo esc_html(sprintf( - /* translators: %s: captcha provider key */ - __('人机验证服务商「%s」已启用但凭据未配齐,前台已跳过渲染。请到「功能&权限 → 登录权限」补全配置。', 'aksrt'), - $provider - )); - echo '
'; - } - return; - } + $scene = sanitize_key((string) $scene); + if (!aksrt_is_captcha_enabled_for($scene)) { + return; + } - $uid = 'aksrt-captcha-' . $scene . '-' . wp_unique_id(); - $vid = 'captcha_verified_' . $scene . '_' . wp_unique_id(); + $cfg = aksrt_get_captcha_config(); + $provider = $cfg['active_provider']; + $pcfg = $cfg[$provider] ?? []; - // 构建 JS 配置 - $js_config = array( - 'provider' => $provider, - 'scene' => $scene, - 'uid' => $uid, - 'vid' => $vid, - 'tc' => array('app_id' => isset($pcfg['app_id']) ? (string) $pcfg['app_id'] : ''), - 'cf' => array( - 'site_key' => isset($pcfg['site_key']) ? (string) $pcfg['site_key'] : '', - 'theme' => isset($pcfg['theme']) ? (string) $pcfg['theme'] : 'auto', - 'size' => isset($pcfg['size']) ? (string) $pcfg['size'] : 'normal' - ), - 'gt' => array( - 'captcha_id' => isset($pcfg['captcha_id']) ? trim((string) $pcfg['captcha_id']) : '', - 'product' => isset($pcfg['product']) && $pcfg['product'] !== '' ? (string) $pcfg['product'] : 'bind', - ), - 'al' => array( - 'region' => isset($pcfg['region']) ? (string) $pcfg['region'] : 'cn', - 'prefix' => isset($pcfg['prefix']) ? (string) $pcfg['prefix'] : '', - 'scene_id' => isset($pcfg['scene_id']) ? (string) $pcfg['scene_id'] : '' - ), - ); - ?> + if (!aksrt_captcha_provider_is_configured($provider, $pcfg)) { + if (current_user_can('manage_options')) { + echo '
'; + echo esc_html(sprintf( + /* translators: %s: captcha provider key */ + __('人机验证服务商「%s」已启用但凭据未配齐,前台已跳过渲染。请到「功能&权限 → 登录权限」补全配置。', 'aksrt'), + $provider + )); + echo '
'; + } + return; + } + + $uid = 'aksrt-captcha-' . $scene . '-' . wp_unique_id(); + $vid = 'captcha_verified_' . $scene . '_' . wp_unique_id(); + + $js_config = [ + 'provider' => $provider, + 'scene' => $scene, + 'uid' => $uid, + 'vid' => $vid, + 'tc' => ['app_id' => (string) ($pcfg['app_id'] ?? '')], + 'cf' => [ + 'site_key' => (string) ($pcfg['site_key'] ?? ''), + 'theme' => (string) ($pcfg['theme'] ?? 'auto'), + 'size' => (string) ($pcfg['size'] ?? 'normal'), + ], + 'gt' => [ + 'captcha_id' => trim((string) ($pcfg['captcha_id'] ?? '')), + 'product' => (string) ($pcfg['product'] ?? 'bind'), + ], + 'al' => [ + 'region' => (string) ($pcfg['region'] ?? 'cn'), + 'prefix' => (string) ($pcfg['prefix'] ?? ''), + 'scene_id' => (string) ($pcfg['scene_id'] ?? ''), + ], + ]; + + $token_fields = ['cf_turnstile_response', 'tencent_ticket', 'tencent_randstr', 'tencent_sid', + 'geetest_lot_number', 'geetest_captcha_output', 'geetest_pass_token', 'geetest_gen_time', + 'aliyun_captcha_verify_param']; + $token_fields_json = wp_json_encode($token_fields, JSON_UNESCAPED_UNICODE); + ?>
- - + +
+ -'; ?> - false, + 'code' => 'captcha_failed', + 'message' => (string) $captcha_result, + ], 400); + } + } + $user = wp_get_current_user(); $commentdata = [ 'comment_post_ID' => $post_id, @@ -263,11 +274,6 @@ class AKSRT_Comment_Actions { $_POST[$k] = $params[$k]; } } - foreach ($params as $k => $v) { - if (is_string($k) && (str_contains($k, 'captcha') || str_starts_with($k, 'g-recaptcha') || str_starts_with($k, 'cf-turnstile') || str_starts_with($k, 'h-captcha'))) { - $_POST[$k] = $v; - } - } $comment_id = wp_new_comment(wp_slash($commentdata), true); if (is_wp_error($comment_id)) { diff --git a/inc/classes/class-aksrt-download.php b/inc/classes/class-aksrt-download.php index ce19aec8..2012a9f1 100644 --- a/inc/classes/class-aksrt-download.php +++ b/inc/classes/class-aksrt-download.php @@ -162,16 +162,17 @@ class AKSRT_Download { if (is_wp_error($gate)) return $gate; $paid_settings = AKSRT_Settings::get_paid_content_settings(); - $need_captcha = !empty($paid_settings['download_captcha']) || !empty($meta['require_captcha']); - if ($need_captcha && function_exists('aksrt_is_captcha_enabled_for') && !aksrt_is_captcha_enabled_for('download')) { - $need_captcha = false; - } - if ($need_captcha && function_exists('aksrt_verify_captcha')) { + $need_captcha = !empty($paid_settings['download_captcha']) || !empty($meta['require_captcha']); + // 下载双开关:cms_features.download_captcha(或单资源强制)且场景未停用。 + if ($need_captcha && function_exists('aksrt_require_captcha')) { $cap_payload = isset($params['captcha']) && is_array($params['captcha']) ? $params['captcha'] : []; - $verify = aksrt_verify_captcha($cap_payload); - if ($verify !== true) { - $msg = is_string($verify) ? $verify : __('人机验证失败', 'aksrt'); - return new WP_Error('aksrt_captcha_failed', $msg, ['status' => 400]); + // 场景未启用时 require 直接放行;启用时强制校验 token。 + if (function_exists('aksrt_is_captcha_enabled_for') && aksrt_is_captcha_enabled_for('download')) { + $verify = aksrt_require_captcha('download', $cap_payload); + if ($verify !== true) { + $msg = is_string($verify) ? $verify : __('人机验证失败', 'aksrt'); + return new WP_Error('aksrt_captcha_failed', $msg, ['status' => 400]); + } } } diff --git a/inc/classes/class-aksrt-signin.php b/inc/classes/class-aksrt-signin.php index 1a849460..2c8e9612 100644 --- a/inc/classes/class-aksrt-signin.php +++ b/inc/classes/class-aksrt-signin.php @@ -95,15 +95,15 @@ class AKSRT_Signin { } // 人机验证(若启用) - if (function_exists('aksrt_is_captcha_enabled_for') && aksrt_is_captcha_enabled_for('signin')) { - $body = $request->get_json_params(); - $cap_payload = isset($body['captcha']) && is_array($body['captcha']) ? $body['captcha'] : []; - if (function_exists('aksrt_verify_captcha')) { - $verify = aksrt_verify_captcha($cap_payload); - if ($verify !== true) { - $msg = is_string($verify) ? $verify : __('人机验证失败', 'aksrt'); - return new WP_Error('aksrt_captcha_failed', $msg, ['status' => 400]); - } + if (function_exists('aksrt_require_captcha')) { + $body = $request->get_json_params(); + $cap_payload = is_array($body) && isset($body['captcha']) && is_array($body['captcha']) + ? $body['captcha'] + : (is_array($body) ? $body : []); + $verify = aksrt_require_captcha('signin', $cap_payload); + if ($verify !== true) { + $msg = is_string($verify) ? $verify : __('人机验证失败', 'aksrt'); + return new WP_Error('aksrt_captcha_failed', $msg, ['status' => 400]); } } diff --git a/inc/classes/class-aksrt-sms.php b/inc/classes/class-aksrt-sms.php index 3ecc1705..5969ff67 100644 --- a/inc/classes/class-aksrt-sms.php +++ b/inc/classes/class-aksrt-sms.php @@ -296,10 +296,15 @@ class AKSRT_SMS { wp_send_json_error(['message' => __('手机号格式不正确', 'aksrt')]); } - // 人机验证钩子(对齐 zibll 的 man_machine_verification) - $captcha_result = apply_filters('aksrt_sms_captcha_verify', true, $phone); - if ($captcha_result !== true) { - wp_send_json_error(['message' => $captcha_result ?: __('人机验证失败', 'aksrt')]); + $scene = sanitize_key((string) ($_POST['scene'] ?? 'register')); + if (!array_key_exists($scene, function_exists('aksrt_captcha_scenes') ? aksrt_captcha_scenes() : [])) { + $scene = 'register'; + } + if (function_exists('aksrt_require_captcha')) { + $captcha_result = aksrt_require_captcha($scene, wp_unslash($_POST)); + if ($captcha_result !== true) { + wp_send_json_error(['message' => $captcha_result ?: __('人机验证失败', 'aksrt')]); + } } $result = $this->send_code($phone); diff --git a/inc/classes/class-aksrt-user.php b/inc/classes/class-aksrt-user.php index ca7fe660..85d4b666 100644 --- a/inc/classes/class-aksrt-user.php +++ b/inc/classes/class-aksrt-user.php @@ -1004,8 +1004,8 @@ add_action('wp_ajax_aksrt_send_email_code', function () { wp_send_json_error( array( 'message' => __( '无效的验证场景', 'aksrt' ) ) ); } - if ( aksrt_is_captcha_enabled_for( $scene ) ) { - $captcha_result = aksrt_verify_captcha( $_POST ); + if ( function_exists( 'aksrt_require_captcha' ) ) { + $captcha_result = aksrt_require_captcha( $scene, wp_unslash( $_POST ) ); if ( $captcha_result !== true ) { wp_send_json_error( array( 'message' => $captcha_result ) ); } @@ -1113,8 +1113,8 @@ add_action('wp_ajax_aksrt_change_password', function () { } // 人机验证 - if ( aksrt_is_captcha_enabled_for( 'change_password' ) ) { - $captcha_result = aksrt_verify_captcha( $_POST ); + if ( function_exists( 'aksrt_require_captcha' ) ) { + $captcha_result = aksrt_require_captcha( 'change_password', wp_unslash( $_POST ) ); if ( $captcha_result !== true ) { wp_send_json_error( array( 'message' => $captcha_result ) ); } @@ -1178,8 +1178,8 @@ add_action('wp_ajax_aksrt_change_email', function () { } // 人机验证 - if ( aksrt_is_captcha_enabled_for( 'change_email' ) ) { - $captcha_result = aksrt_verify_captcha( $_POST ); + if ( function_exists( 'aksrt_require_captcha' ) ) { + $captcha_result = aksrt_require_captcha( 'change_email', wp_unslash( $_POST ) ); if ( $captcha_result !== true ) { wp_send_json_error( array( 'message' => $captcha_result ) ); } diff --git a/inc/classes/scripts-traits/trait-aksrt-scripts-part-1b.php b/inc/classes/scripts-traits/trait-aksrt-scripts-part-1b.php index 4a408bb7..6dfd2728 100644 --- a/inc/classes/scripts-traits/trait-aksrt-scripts-part-1b.php +++ b/inc/classes/scripts-traits/trait-aksrt-scripts-part-1b.php @@ -886,18 +886,6 @@ trait AKSRT_Scripts_Part_1b { ); } - // Slider CAPTCHA (canvas-based puzzle verification) - $slider_captcha_js = get_template_directory() . '/assets/js/slider-captcha.js'; - if (file_exists($slider_captcha_js)) { - wp_enqueue_script( - 'aksrt-slider-captcha', - $base_uri . '/assets/js/slider-captcha.js', - [], - (string) filemtime($slider_captcha_js), - true - ); - } - // Instant page prefetch (hover to preload) $instant_page_js = get_template_directory() . '/assets/js/instant-page.js'; if (file_exists($instant_page_js)) { diff --git a/inc/classes/settings/class-aksrt-settings-capabilities.php b/inc/classes/settings/class-aksrt-settings-capabilities.php index b750ac2f..7333d4ae 100644 --- a/inc/classes/settings/class-aksrt-settings-capabilities.php +++ b/inc/classes/settings/class-aksrt-settings-capabilities.php @@ -182,63 +182,47 @@ class AKSRT_Settings_Capabilities { } /** - * 获取"网站安全"设置 + * 获取"网站安全"设置(人机验证见 aksrt_get_captcha_status_summary)。 */ public static function get_security_settings(): array { $s = AKSRT_Settings::get_all(); - $verification_type = sanitize_key((string) ($s['user_verification_type'] ?? 'slider')); - $allowed_types = ['image', 'slider', 'tcaptcha', 'geetest', 'null']; - if (!in_array($verification_type, $allowed_types, true)) { - $verification_type = 'slider'; - } + $captcha = function_exists('aksrt_get_captcha_status_summary') + ? aksrt_get_captcha_status_summary() + : ['enabled' => false, 'provider' => '', 'configured' => false, 'scenes' => []]; return [ - 'verification_type' => $verification_type, - 'verification_comment_s' => !empty($s['verification_comment_s']), - 'verification_newposts_s' => !empty($s['verification_newposts_s']), - 'verification_bbspost_s' => !empty($s['verification_bbspost_s']), - 'verification_links_s' => !empty($s['verification_links_s'] ?? true), - 'verification_signin_exclude' => sanitize_text_field((string) ($s['verification_signin_exclude'] ?? '')), - 'geetest_enabled' => !empty($s['geetest_enabled']), - 'geetest_id' => sanitize_text_field((string) ($s['geetest_id'] ?? '')), - 'geetest_key' => sanitize_text_field((string) ($s['geetest_key'] ?? '')), - 'tcaptcha_enabled' => !empty($s['tcaptcha_enabled']), - 'tcaptcha_appid' => sanitize_text_field((string) ($s['tcaptcha_appid'] ?? '')), - 'tcaptcha_secret_key' => sanitize_text_field((string) ($s['tcaptcha_secret_key'] ?? '')), - 'brush_limit_post' => [ + 'captcha' => $captcha, + 'brush_limit_post' => [ 'enabled' => !empty($s['brush_limit_post_enabled']), 'minutes_10' => (int) ($s['brush_limit_post_minutes_10'] ?? 20), 'day_1' => (int) ($s['brush_limit_post_day_1'] ?? 50), ], - 'brush_limit_comment' => [ + 'brush_limit_comment' => [ 'enabled' => !empty($s['brush_limit_comment_enabled']), 'minutes_10' => (int) ($s['brush_limit_comment_minutes_10'] ?? 20), 'day_1' => (int) ($s['brush_limit_comment_day_1'] ?? 100), ], - 'brush_limit_upload' => [ + 'brush_limit_upload' => [ 'enabled' => !empty($s['brush_limit_upload_enabled']), 'minutes_10' => (int) ($s['brush_limit_upload_minutes_10'] ?? 50), 'day_1' => (int) ($s['brush_limit_upload_day_1'] ?? 200), ], - 'brush_limit_register' => [ + 'brush_limit_register' => [ 'enabled' => !empty($s['brush_limit_register_enabled']), 'minutes_10' => (int) ($s['brush_limit_register_minutes_10'] ?? 20), 'day_1' => (int) ($s['brush_limit_register_day_1'] ?? 100), ], - 'brush_limit_order' => [ + 'brush_limit_order' => [ 'enabled' => !empty($s['brush_limit_order_enabled']), 'minutes_10' => (int) ($s['brush_limit_order_minutes_10'] ?? 30), 'day_1' => (int) ($s['brush_limit_order_day_1'] ?? 100), ], - 'error_frequency_limit' => !empty($s['error_frequency_limit'] ?? true), - 'user_disable_admin' => !empty($s['user_disable_admin'] ?? true), - 'disabled_pingback' => !empty($s['disabled_pingback'] ?? true), - 'disabled_xmlrpc' => !empty($s['disabled_xmlrpc'] ?? true), - 'close_attachment_page' => !empty($s['close_attachment_page'] ?? true), + 'error_frequency_limit' => !empty($s['error_frequency_limit'] ?? true), ]; } + /** * 获取"系统工具"设置 */