feat(captcha): 打通人机验证 11 个场景的前后端完整链路

- 统一验证入口 aksrt_require_captcha(),返回 true|WP_Error,启用场景配置缺失时 fail-closed 拒绝
- 前端公共 API 重构为 window.AKSRTCaptcha(verify/collect/reset/run),单飞锁防并发重复验证,一次性 proof 绑定浏览器 cookie+场景+服务商+主机名
- challenge/exchange REST 端点:challenge 原子占用防票据放大厂商调用,增加频率限制
- 新增 OAuth bind pass:服务端签发 5 分钟一次性令牌,绑定用户+服务商+浏览器,微信扫码延迟消费
- 覆盖场景:评论、登录、注册、绑定第三方、绑定手机、找回密码、修改密码、更改邮箱、资料下载、签到、前台投稿
- 移除 Service 层重复验证与 _auth_code_verified/_defer_login 等客户端可伪造标志,删除下载共享 pass 死代码
- 修复腾讯 TC3 签名 scope、Cloudflare action/cdata/hostname 严格校验、阿里云验签错误码映射
- 后台 Schema 保存对 secret 字段做掩码/保留合并;导入导出过滤 aksrt_captcha_config
- 重建 public bundle 与 admin SPA;新增 captcha 运行时/安全/业务契约测试与 harness
This commit is contained in:
kate522 committed 2026-09-02 13:03:24 +08:00
1 parent d2e95acd75
commit ff4ec1eb43
45 files changed
+2694 -1435

No files matched your search

+25 -83
View File
@@ -192,8 +192,8 @@
}
}
// 执行实际的 AJAX 请求 — local insert instead of full-page re-fetch
function performSubmit() {
// 执行实际的 REST 请求 — local insert instead of full-page re-fetch
function performSubmit(captchaPayload) {
setLoading(true);
var formData = collectFormData();
@@ -205,6 +205,7 @@
// Try REST endpoint for local-insert HTML response
var restBody = {};
formData.forEach(function (v, k) { restBody[k] = v; });
Object.assign(restBody, captchaPayload || {});
if (isEdit) {
restBody.comment_id = parseInt(editId.value, 10);
restBody.content = restBody.comment || restBody.content || '';
@@ -245,15 +246,6 @@
var replyInfo = document.getElementById('comment-reply-info');
if (replyInfo) replyInfo.style.display = 'none';
// 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) {
@@ -282,14 +274,6 @@
if (json && json.code) {
notyf(json.message || '评论提交失败', 'danger');
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;
@@ -299,9 +283,14 @@
setLoading(false);
setTimeout(function () { reloadCommentList(); }, 500);
})
.catch(function () {
// Fallback: submit to wp-comments-post.php directly
performLegacySubmit();
.catch(function (error) {
console.error('[AKSRT Comment] 提交失败:', error);
notyf((error && error.message) || '网络连接失败,请检查网络后重试', 'danger');
setLoading(false);
})
.finally(function () {
if (window.AKSRTCaptcha) window.AKSRTCaptcha.reset('comment', form);
if (!countdownTimer && isSubmitting) setLoading(false);
});
}
@@ -373,44 +362,6 @@
placeholder.remove();
}
}
// Legacy fallback: POST to wp-comments-post.php
function performLegacySubmit() {
var formData = collectFormData();
var actionUrl = form.getAttribute('action') || '/wp-comments-post.php';
fetch(actionUrl, {
method: 'POST',
body: formData,
credentials: 'same-origin',
headers: { 'X-Requested-With': 'XMLHttpRequest' }
})
.then(function (response) {
return response.text().then(function (text) {
return { status: response.status, text: text, ok: response.ok };
});
})
.then(function (result) {
handleResponse(result.text, result.status);
})
.catch(function (error) {
console.error('[AKSRT Comment] 提交失败:', error);
if (error.message === 'Failed to fetch' || error.name === 'TypeError') {
notyf('网络连接失败,请检查网络后重试', 'danger');
} else {
notyf(error.message || '评论提交失败,请重试', 'danger');
}
setLoading(false);
})
.finally(function () {
// 兜底:避免异常分支漏掉状态恢复导致按钮卡在"发送中"
if (!countdownTimer && isSubmitting) {
setLoading(false);
}
});
}
// 核心表单提交事件处理器
function handleSubmit(e) {
// 如果已经在提交中或倒计时中,防止重复提交
@@ -423,29 +374,20 @@
// 阻止默认表单提交
e.preventDefault();
e.stopImmediatePropagation();
// 检查验证码状态(如果有验证码组件)
var captchaScene = window.AKSRTCaptchaState &&
window.AKSRTCaptchaState.scenes &&
window.AKSRTCaptchaState.scenes.comment;
if (captchaScene && typeof captchaScene.verify === 'function') {
// 有验证码:先触发验证流程
setLoading(true);
captchaScene.verify(function (verified) {
if (verified !== false) {
// 验证通过,执行 AJAX 提交
performSubmit();
} else {
// 验证失败
notyf('人机验证未通过,请重试', 'warning');
setLoading(false);
}
});
} else {
// 无验证码或已经验证过:直接提交
performSubmit();
}
var captcha = window.AKSRTCaptcha;
setLoading(true);
(captcha ? captcha.run('comment', form) : Promise.resolve({})).then(function (captchaPayload) {
if (captchaPayload === null) {
notyf('人机验证未通过,请重试', 'warning');
setLoading(false);
return;
}
performSubmit(captchaPayload);
}).catch(function () {
notyf('人机验证未通过,请重试', 'warning');
setLoading(false);
if (captcha) captcha.reset('comment', form);
});
return false;
}