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:
1 parent
d2e95acd75
commit
ff4ec1eb43
45 files changed
+2694
-1435
No files matched your search
+51
-26
@@ -26,6 +26,7 @@
|
||||
account_match_failed: '第三方账号匹配失败,请重试',
|
||||
login_failed: '第三方登录失败,请重试',
|
||||
bind_failed: '第三方账号绑定失败,请重试',
|
||||
bind_verification_required: '请先完成人机验证后再绑定',
|
||||
binding_conflict: '该第三方账号已绑定其他账号',
|
||||
account_conflict: '该第三方账号无法绑定到当前账号',
|
||||
already_bound: '该第三方账号已绑定其他账号',
|
||||
@@ -297,10 +298,12 @@
|
||||
|
||||
nodes.box.dataset.loading = '1';
|
||||
setQrStatus(root, '正在获取二维码');
|
||||
qrRequest(root, 'qrcode-login', {
|
||||
var qrPayload = {
|
||||
action: root.dataset.oauthIntent || 'login',
|
||||
redirect_to: root.dataset.redirectTo || window.location.href,
|
||||
}).then(function (result) {
|
||||
};
|
||||
if (root.dataset.bindPass) qrPayload.bind_pass = root.dataset.bindPass;
|
||||
qrRequest(root, 'qrcode-login', qrPayload).then(function (result) {
|
||||
if (!result || !isRootActive(root)) return;
|
||||
nodes.box.dataset.loading = '0';
|
||||
nodes.box.dataset.loaded = '1';
|
||||
@@ -347,6 +350,7 @@
|
||||
var root = form.closest('[data-aksrt-auth]');
|
||||
data.action = root ? (root.dataset.oauthIntent || 'login') : 'login';
|
||||
data.redirect_to = root ? (root.dataset.redirectTo || window.location.href) : window.location.href;
|
||||
if (root && root.dataset.bindPass) data.bind_pass = root.dataset.bindPass;
|
||||
var button = form.querySelector('button[type="submit"]');
|
||||
if (button) button.disabled = true;
|
||||
request('weixingzh-code', data).then(function (result) {
|
||||
@@ -391,23 +395,20 @@
|
||||
}
|
||||
|
||||
function captchaScene(scene) {
|
||||
return scene === 'resetpassword' ? 'lostpassword' : scene;
|
||||
return scene === 'resetpassword' || scene === 'reset-password' ? 'lostpassword' : scene;
|
||||
}
|
||||
|
||||
function verifyCaptcha(scene, callback) {
|
||||
var state = window.AKSRTCaptchaState;
|
||||
var captcha = state && state.scenes ? state.scenes[captchaScene(scene)] : null;
|
||||
if (!captcha || typeof captcha.verify !== 'function') {
|
||||
callback(true);
|
||||
return;
|
||||
}
|
||||
captcha.verify(function (verified) { callback(verified !== false); });
|
||||
function captchaFormScene(form, fallback) {
|
||||
var mount = form && form.querySelector ? form.querySelector('[data-aksrt-captcha-config][data-scene]') : null;
|
||||
return mount ? (mount.getAttribute('data-scene') || captchaScene(fallback)) : captchaScene(fallback);
|
||||
}
|
||||
|
||||
function resetCaptcha(scene) {
|
||||
var state = window.AKSRTCaptchaState;
|
||||
var captcha = state && state.scenes ? state.scenes[captchaScene(scene)] : null;
|
||||
if (captcha && typeof captcha.reset === 'function') captcha.reset();
|
||||
function captchaScope(form) {
|
||||
return form || document;
|
||||
}
|
||||
|
||||
function resetCaptcha(scene, scope) {
|
||||
if (window.AKSRTCaptcha) window.AKSRTCaptcha.reset(captchaScene(scene), captchaScope(scope));
|
||||
}
|
||||
|
||||
function sendCode(button) {
|
||||
@@ -422,39 +423,63 @@
|
||||
payload.scene = button.dataset.sendCode;
|
||||
payload.target = target.value.trim();
|
||||
payload.channel = (form.querySelector('[name="channel"]') || {}).value || '';
|
||||
verifyCaptcha(payload.scene, function (verified) {
|
||||
if (!verified) {
|
||||
var scene = captchaFormScene(form, payload.scene);
|
||||
var captcha = window.AKSRTCaptcha;
|
||||
var ticket = captcha ? captcha.run(captchaScene(scene), captchaScope(form)) : Promise.resolve({});
|
||||
ticket.then(function (captchaPayload) {
|
||||
if (captchaPayload === null) {
|
||||
button.disabled = false;
|
||||
notify('请先完成人机验证', 'warning');
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
payload = formData(form);
|
||||
payload.scene = button.dataset.sendCode;
|
||||
payload.target = target.value.trim();
|
||||
payload.channel = (form.querySelector('[name="channel"]') || {}).value || '';
|
||||
request('code', payload).then(function (result) {
|
||||
Object.assign(payload, captchaPayload || {});
|
||||
return request('code', payload).then(function (result) {
|
||||
notify(result.message || '验证码已发送', 'success');
|
||||
startCountdown(button, result.countdown || 60);
|
||||
}).catch(function (error) {
|
||||
button.disabled = false;
|
||||
notify(error.message, 'danger');
|
||||
}).finally(function () {
|
||||
resetCaptcha(payload.scene);
|
||||
resetCaptcha(scene, form);
|
||||
});
|
||||
}).catch(function (error) {
|
||||
button.disabled = false;
|
||||
resetCaptcha(scene, form);
|
||||
notify(error.message || '人机验证失败,请重试', 'danger');
|
||||
});
|
||||
}
|
||||
|
||||
function submit(form) {
|
||||
var action = form.dataset.authForm;
|
||||
var payload = formData(form);
|
||||
if (action === 'register' && payload.channel !== 'phone') payload.user_email = payload.target;
|
||||
var scene = captchaFormScene(form, action);
|
||||
var captcha = window.AKSRTCaptcha;
|
||||
setBusy(form, true);
|
||||
request(action, payload).then(function (result) {
|
||||
notify(result.message || '操作成功', 'success');
|
||||
if (result.redirect) window.setTimeout(function () { window.location.assign(result.redirect); }, 350);
|
||||
(captcha ? captcha.run(scene, captchaScope(form)) : Promise.resolve({})).then(function (captchaPayload) {
|
||||
if (captchaPayload === null) {
|
||||
setBusy(form, false);
|
||||
notify('请先完成人机验证', 'warning');
|
||||
return null;
|
||||
}
|
||||
var payload = formData(form);
|
||||
if (action === 'register' && payload.channel !== 'phone') payload.user_email = payload.target;
|
||||
Object.assign(payload, captchaPayload || {});
|
||||
return request(action, payload).then(function (result) {
|
||||
notify(result.message || '操作成功', 'success');
|
||||
if (result.redirect) window.setTimeout(function () { window.location.assign(result.redirect); }, 350);
|
||||
}).catch(function (error) {
|
||||
notify(error.message, 'danger');
|
||||
setBusy(form, false);
|
||||
}).finally(function () {
|
||||
resetCaptcha(scene, form);
|
||||
});
|
||||
}).catch(function (error) {
|
||||
notify(error.message, 'danger');
|
||||
setBusy(form, false);
|
||||
resetCaptcha(scene, form);
|
||||
notify(error.message || '人机验证失败,请重试', 'danger');
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user