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:
+424
-273
@@ -1,406 +1,557 @@
|
||||
/**
|
||||
* AKSRT Captcha front-end runtime — cloud providers only (tencent / aliyun / cloudflare / geetest).
|
||||
* AKSRT Captcha front-end runtime — cloud providers only.
|
||||
*
|
||||
* PHP renders passive mounts via [data-aksrt-captcha-config] (JSON config attribute);
|
||||
* this script scans them, registers AKSRTCaptchaState.scenes[scene], loads the provider
|
||||
* SDK once per page, and writes verified tokens back into the surrounding form.
|
||||
* Bundled into assets/js/build/public.js by tools/build-public-assets.mjs.
|
||||
* Mounts are registered internally by uid and resolved by scene plus DOM scope.
|
||||
* The only public surface is window.AKSRTCaptcha.
|
||||
*/
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
var TOKEN_FIELDS = [
|
||||
var TOKEN_FIELDS = Object.freeze([
|
||||
'captcha_verified',
|
||||
'aksrt_captcha_challenge',
|
||||
'aksrt_captcha_proof',
|
||||
'cf_turnstile_response',
|
||||
'tencent_ticket', 'tencent_randstr', 'tencent_sid',
|
||||
'tencent_ticket', 'tencent_randstr',
|
||||
'geetest_lot_number', 'geetest_captcha_output', 'geetest_pass_token', 'geetest_gen_time',
|
||||
'aliyun_captcha_verify_param'
|
||||
];
|
||||
]);
|
||||
var loaders = {};
|
||||
var instances = {};
|
||||
var scenes = {};
|
||||
|
||||
var root = (window.AKSRTCaptchaState = window.AKSRTCaptchaState || { loaders: {}, scenes: {} });
|
||||
root.rescan = scan;
|
||||
function asElement(scope) {
|
||||
if (!scope) return null;
|
||||
if (scope.nodeType === 1 || scope.nodeType === 9) return scope;
|
||||
if (typeof scope !== 'string') return null;
|
||||
if (instances[scope]) return instances[scope].box;
|
||||
try { return document.querySelector(scope); } catch (_) { return null; }
|
||||
}
|
||||
|
||||
function scan() {
|
||||
var mounts = document.querySelectorAll('[data-aksrt-captcha-config]');
|
||||
Array.prototype.forEach.call(mounts, function (el) {
|
||||
if (el.dataset.aksrtCaptchaBound) {
|
||||
return;
|
||||
}
|
||||
el.dataset.aksrtCaptchaBound = '1';
|
||||
var config;
|
||||
function scopeBoundary(box, config) {
|
||||
if (config.scope) {
|
||||
try {
|
||||
config = JSON.parse(el.getAttribute('data-aksrt-captcha-config'));
|
||||
} catch (e) {
|
||||
return;
|
||||
}
|
||||
if (config && config.scene && config.uid && config.vid) {
|
||||
initMount(config);
|
||||
}
|
||||
var configured = document.querySelector(config.scope);
|
||||
if (configured) return configured;
|
||||
} catch (_) {}
|
||||
}
|
||||
return box.closest('[data-aksrt-captcha-scope], form, [data-aksrt-download-box], [data-aksrt-auth], .aksrt-modal, [data-signin-captcha], #comments') || box.parentElement;
|
||||
}
|
||||
|
||||
function register(instance) {
|
||||
instances[instance.uid] = instance;
|
||||
scenes[instance.scene] = scenes[instance.scene] || [];
|
||||
if (scenes[instance.scene].indexOf(instance) === -1) scenes[instance.scene].push(instance);
|
||||
}
|
||||
|
||||
function resolve(scene, scope) {
|
||||
scan();
|
||||
if (typeof scope === 'string' && instances[scope] && instances[scope].scene === scene) {
|
||||
return instances[scope];
|
||||
}
|
||||
var candidates = scenes[scene] || [];
|
||||
var element = asElement(scope);
|
||||
if (element && element.getAttribute) {
|
||||
var uid = element.getAttribute('data-aksrt-captcha-uid') || element.getAttribute('data-captcha-uid');
|
||||
if (uid && instances[uid] && instances[uid].scene === scene) return instances[uid];
|
||||
}
|
||||
if (!element) return scope ? null : (candidates.length === 1 ? candidates[0] : null);
|
||||
var matches = candidates.filter(function (instance) {
|
||||
var boundary = instance.scope;
|
||||
return element === instance.box || element === instance.mount || element === boundary ||
|
||||
instance.box.contains(element) || instance.mount.contains(element) ||
|
||||
(boundary && boundary.contains(element)) ||
|
||||
(element.contains && (element.contains(instance.box) || (boundary && element.contains(boundary))));
|
||||
});
|
||||
if (matches.length === 1) return matches[0];
|
||||
if (!matches.length) return null;
|
||||
matches.sort(function (a, b) {
|
||||
if (a.scope === b.scope) return 0;
|
||||
if (a.scope && b.scope && a.scope.contains(b.scope)) return 1;
|
||||
if (a.scope && b.scope && b.scope.contains(a.scope)) return -1;
|
||||
return 0;
|
||||
});
|
||||
return matches[0];
|
||||
}
|
||||
|
||||
function verify(scene, scope) {
|
||||
var instance = resolve(scene, scope);
|
||||
if (!instance) return Promise.resolve(true);
|
||||
return instance.verify();
|
||||
}
|
||||
|
||||
function collect(scene, scope) {
|
||||
var instance = resolve(scene, scope);
|
||||
return instance ? instance.collect() : {};
|
||||
}
|
||||
|
||||
function reset(scene, scope) {
|
||||
var instance = resolve(scene, scope);
|
||||
if (instance) instance.reset();
|
||||
}
|
||||
|
||||
function releaseRunLock(instance) {
|
||||
instance.runLocked = false;
|
||||
var waiters = instance.lockWaiters.slice();
|
||||
instance.lockWaiters = [];
|
||||
waiters.forEach(function (resolveWaiter) { resolveWaiter(null); });
|
||||
}
|
||||
|
||||
function run(scene, scope) {
|
||||
var instance = resolve(scene, scope);
|
||||
if (!instance) return Promise.resolve({});
|
||||
if (instance.runLocked) {
|
||||
return new Promise(function (resolveWaiter) { instance.lockWaiters.push(resolveWaiter); });
|
||||
}
|
||||
instance.runLocked = true;
|
||||
return instance.verify().then(function (passed) {
|
||||
if (!passed) releaseRunLock(instance);
|
||||
return passed ? instance.collect() : null;
|
||||
}).catch(function (error) {
|
||||
releaseRunLock(instance);
|
||||
throw error;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* One mount = one scene instance. Mirrors the scene semantics of the previous
|
||||
* per-render inline script: the last initialized mount wins scenes[scene].
|
||||
*/
|
||||
function initMount(C) {
|
||||
var root = window.AKSRTCaptchaState;
|
||||
var box, hField, form = null;
|
||||
window.AKSRTCaptcha = Object.freeze({
|
||||
verify: verify,
|
||||
collect: collect,
|
||||
reset: reset,
|
||||
run: run,
|
||||
TOKEN_FIELDS: TOKEN_FIELDS
|
||||
});
|
||||
|
||||
function scan() {
|
||||
var mounts = document.querySelectorAll('[data-aksrt-captcha-config]');
|
||||
Array.prototype.forEach.call(mounts, function (mount) {
|
||||
if (mount.dataset.aksrtCaptchaBound) return;
|
||||
var config;
|
||||
try {
|
||||
config = JSON.parse(mount.getAttribute('data-aksrt-captcha-config'));
|
||||
} catch (_) {
|
||||
return;
|
||||
}
|
||||
if (!config || !config.scene || !config.uid || !config.vid) return;
|
||||
mount.dataset.aksrtCaptchaBound = '1';
|
||||
initMount(config, mount);
|
||||
});
|
||||
}
|
||||
|
||||
function initMount(C, mount) {
|
||||
var box = document.getElementById(C.uid);
|
||||
var hField = document.getElementById(C.vid);
|
||||
if (!box || !hField) return;
|
||||
|
||||
var container = box.closest('form') || mount;
|
||||
mount.setAttribute('data-aksrt-captcha-uid', C.uid);
|
||||
var verified = false;
|
||||
var pendingAction = null;
|
||||
var triggerCaptcha = function () { showTip('验证码加载中,请稍候', '#e67e22'); };
|
||||
var pending = [];
|
||||
var refreshingChallenge = null;
|
||||
var providerReset = function () {};
|
||||
var triggerCaptcha = function () {
|
||||
showTip('验证码加载中,请稍候', '#e67e22');
|
||||
finishPending(false);
|
||||
};
|
||||
|
||||
function loadScriptOnce(key, src, readyCheck) {
|
||||
if (typeof readyCheck === 'function' && readyCheck()) { return Promise.resolve(); }
|
||||
if (root.loaders[key]) { return root.loaders[key]; }
|
||||
root.loaders[key] = new Promise(function (resolve, reject) {
|
||||
if (typeof readyCheck === 'function' && readyCheck()) return Promise.resolve();
|
||||
if (loaders[key]) return loaders[key];
|
||||
loaders[key] = new Promise(function (resolveLoad, rejectLoad) {
|
||||
var existing = document.querySelector('script[data-aksrt-sdk="' + key + '"]');
|
||||
if (existing) {
|
||||
if (typeof readyCheck === 'function' && readyCheck()) { resolve(); return; }
|
||||
existing.addEventListener('load', function () { resolve(); }, { once: true });
|
||||
existing.addEventListener('error', function () { reject(new Error(key + ' load failed')); }, { once: true });
|
||||
if (typeof readyCheck === 'function' && readyCheck()) { resolveLoad(); return; }
|
||||
existing.addEventListener('load', resolveLoad, { once: true });
|
||||
existing.addEventListener('error', function () { rejectLoad(new Error(key + ' load failed')); }, { once: true });
|
||||
return;
|
||||
}
|
||||
var script = document.createElement('script');
|
||||
script.src = src;
|
||||
script.async = true;
|
||||
script.setAttribute('data-aksrt-sdk', key);
|
||||
script.onload = function () { resolve(); };
|
||||
script.onerror = function () { reject(new Error(key + ' load failed')); };
|
||||
script.onload = resolveLoad;
|
||||
script.onerror = function () { rejectLoad(new Error(key + ' load failed')); };
|
||||
document.head.appendChild(script);
|
||||
});
|
||||
return root.loaders[key];
|
||||
}
|
||||
|
||||
function findForm() {
|
||||
form = box.closest('form');
|
||||
if (!form && C.scene === 'comment') form = document.getElementById('commentform');
|
||||
if (!form && (C.scene === 'login' || C.scene === 'register')) {
|
||||
form = box.closest('[data-auth-form]') || document.querySelector('[data-auth-form]');
|
||||
}
|
||||
if (!form && C.scene === 'bind_phone') {
|
||||
form = document.getElementById('aksrt-bind-phone-form')
|
||||
|| document.querySelector('[data-aksrt-bind-phone-form]')
|
||||
|| document.querySelector('.auth-form');
|
||||
}
|
||||
return form;
|
||||
}
|
||||
|
||||
function bindSubmit() {
|
||||
if (!form) return;
|
||||
form.addEventListener('submit', function (e) {
|
||||
if (verified || hField.value === '1') return;
|
||||
e.preventDefault(); e.stopImmediatePropagation();
|
||||
pendingAction = null; triggerCaptcha();
|
||||
}, true);
|
||||
return loaders[key];
|
||||
}
|
||||
|
||||
function setField(name, value) {
|
||||
var container = form || box;
|
||||
var el = container.querySelector('input[name="' + name + '"]');
|
||||
if (!el) {
|
||||
el = document.createElement('input');
|
||||
el.type = 'hidden';
|
||||
el.name = name;
|
||||
container.appendChild(el);
|
||||
var field = container.querySelector('input[name="' + name + '"]');
|
||||
if (!field) {
|
||||
field = document.createElement('input');
|
||||
field.type = 'hidden';
|
||||
field.name = name;
|
||||
container.appendChild(field);
|
||||
}
|
||||
el.value = value;
|
||||
field.value = value == null ? '' : value;
|
||||
}
|
||||
|
||||
function collectFields() {
|
||||
var payload = {};
|
||||
TOKEN_FIELDS.forEach(function (name) {
|
||||
var field = container.querySelector('input[name="' + name + '"]');
|
||||
if (field && field.value !== '') payload[name] = field.value;
|
||||
});
|
||||
return payload;
|
||||
}
|
||||
|
||||
function refreshChallenge() {
|
||||
if (!C.challenge_url || typeof window.fetch !== 'function') return Promise.resolve();
|
||||
if (refreshingChallenge) return refreshingChallenge;
|
||||
refreshingChallenge = window.fetch(C.challenge_url, {
|
||||
method: 'POST',
|
||||
credentials: 'same-origin',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ scene: C.scene })
|
||||
}).then(function (response) {
|
||||
return response.json().catch(function () { return {}; }).then(function (body) {
|
||||
if (!response.ok || !body || body.success === false) throw new Error(body.message || 'challenge refresh failed');
|
||||
var data = body.data || body;
|
||||
if (!data.challenge) throw new Error('challenge missing');
|
||||
setField('aksrt_captcha_challenge', data.challenge);
|
||||
});
|
||||
}).catch(function () {
|
||||
showTip('无法刷新人机验证会话,请刷新页面重试', '#e74c3c');
|
||||
}).finally(function () {
|
||||
refreshingChallenge = null;
|
||||
});
|
||||
return refreshingChallenge;
|
||||
}
|
||||
|
||||
function clearProviderTokens() {
|
||||
TOKEN_FIELDS.forEach(function (name) {
|
||||
if (name === 'captcha_verified' || name === 'aksrt_captcha_challenge' || name === 'aksrt_captcha_proof') return;
|
||||
Array.prototype.forEach.call(container.querySelectorAll('input[name="' + name + '"]'), function (field) {
|
||||
field.remove();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function resetVerificationState() {
|
||||
verified = false;
|
||||
releaseRunLock(instance);
|
||||
hField.value = '0';
|
||||
var container = form || box;
|
||||
TOKEN_FIELDS.forEach(function (n) {
|
||||
var f = container.querySelector('input[name="' + n + '"]');
|
||||
if (f) f.remove();
|
||||
});
|
||||
if (C.scene === 'download') {
|
||||
syncDownloadSiblings(false);
|
||||
}
|
||||
setField('aksrt_captcha_proof', '');
|
||||
clearProviderTokens();
|
||||
try { providerReset(); } catch (_) {}
|
||||
refreshChallenge();
|
||||
}
|
||||
|
||||
function syncDownloadSiblings(passed) {
|
||||
var myContainer = box.closest('[data-aksrt-download-captcha]');
|
||||
if (!myContainer) return;
|
||||
document.querySelectorAll('[data-aksrt-download-captcha]').forEach(function (target) {
|
||||
if (target === myContainer) return;
|
||||
var tgtH = target.querySelector('input[name="captcha_verified"]');
|
||||
if (tgtH) tgtH.value = passed ? '1' : '0';
|
||||
TOKEN_FIELDS.forEach(function (name) {
|
||||
var f = target.querySelector('input[name="' + name + '"]');
|
||||
if (f) f.remove();
|
||||
if (passed) {
|
||||
var srcEl = myContainer.querySelector('input[name="' + name + '"]');
|
||||
if (srcEl && srcEl.value !== '') {
|
||||
var tgtEl = document.createElement('input');
|
||||
tgtEl.type = 'hidden';
|
||||
tgtEl.name = name;
|
||||
target.appendChild(tgtEl);
|
||||
tgtEl.value = srcEl.value;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function submitNativeForm() {
|
||||
if (!form) return;
|
||||
if (form.requestSubmit) { form.requestSubmit(); }
|
||||
else { HTMLFormElement.prototype.submit.call(form); }
|
||||
}
|
||||
|
||||
function submitForm() {
|
||||
verified = true; hField.value = '1';
|
||||
if (C.scene === 'download') syncDownloadSiblings(true);
|
||||
if (typeof pendingAction === 'function') {
|
||||
finishPending(true);
|
||||
return;
|
||||
}
|
||||
setTimeout(resetVerificationState, 3000);
|
||||
submitNativeForm();
|
||||
}
|
||||
|
||||
function showTip(msg, color) {
|
||||
var t = box.querySelector('.captcha-tip');
|
||||
if (!t) {
|
||||
t = document.createElement('div');
|
||||
t.className = 'captcha-tip';
|
||||
t.style.cssText = 'font-size:12px;margin-top:4px;text-align:left;';
|
||||
box.appendChild(t);
|
||||
}
|
||||
t.style.color = color || '#e74c3c';
|
||||
t.textContent = msg;
|
||||
setTimeout(function () { if (t.parentNode) t.remove(); }, 4000);
|
||||
}
|
||||
|
||||
function showLoading() { box.innerHTML = '<div style="text-align:center;padding:20px;color:#9ca3af;">验证码加载中...</div>'; }
|
||||
function clearLoading() { box.innerHTML = ''; }
|
||||
|
||||
function finishPending(ok) {
|
||||
var action = pendingAction;
|
||||
pendingAction = null;
|
||||
if (typeof action === 'function') { action(!!ok); }
|
||||
var callbacks = pending.slice();
|
||||
pending = [];
|
||||
callbacks.forEach(function (callback) { callback(!!ok); });
|
||||
}
|
||||
|
||||
function cancelPending(msg) {
|
||||
if (msg) showTip(msg, '#e67e22');
|
||||
function cancelPending(message) {
|
||||
if (message) showTip(message, '#e67e22');
|
||||
finishPending(false);
|
||||
}
|
||||
|
||||
root.scenes[C.scene] = {
|
||||
verify: function (callback) {
|
||||
if (verified || hField.value === '1') {
|
||||
if (typeof callback === 'function') callback(true);
|
||||
return;
|
||||
}
|
||||
pendingAction = (typeof callback === 'function') ? function (ok) { callback(ok !== false); } : null;
|
||||
triggerCaptcha();
|
||||
},
|
||||
reset: resetVerificationState
|
||||
function providerPayload() {
|
||||
var payload = collectFields();
|
||||
payload.scene = C.scene;
|
||||
return payload;
|
||||
}
|
||||
|
||||
function exchangeVerification() {
|
||||
if (!C.exchange_url || typeof window.fetch !== 'function') {
|
||||
cancelPending('人机验证服务不可用,请刷新页面重试');
|
||||
return;
|
||||
}
|
||||
window.fetch(C.exchange_url, {
|
||||
method: 'POST',
|
||||
credentials: 'same-origin',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(providerPayload())
|
||||
}).then(function (response) {
|
||||
return response.json().catch(function () { return {}; }).then(function (body) {
|
||||
if (!response.ok || !body || body.success === false) throw new Error(body.message || '人机验证失败');
|
||||
var data = body.data || body;
|
||||
if (!data.proof) throw new Error('人机验证凭证缺失');
|
||||
setField('aksrt_captcha_proof', data.proof);
|
||||
clearProviderTokens();
|
||||
completeVerification();
|
||||
});
|
||||
}).catch(function (error) {
|
||||
cancelPending(error.message || '人机验证失败,请重试');
|
||||
resetVerificationState();
|
||||
});
|
||||
}
|
||||
|
||||
function completeProviderVerification() {
|
||||
hField.value = '1';
|
||||
exchangeVerification();
|
||||
}
|
||||
|
||||
function completeVerification() {
|
||||
verified = true;
|
||||
hField.value = '1';
|
||||
finishPending(true);
|
||||
}
|
||||
|
||||
function showTip(message, color) {
|
||||
var tip = box.querySelector('.captcha-tip');
|
||||
if (!tip) {
|
||||
tip = document.createElement('div');
|
||||
tip.className = 'captcha-tip';
|
||||
tip.style.cssText = 'font-size:12px;margin-top:4px;text-align:left;';
|
||||
box.appendChild(tip);
|
||||
}
|
||||
tip.style.color = color || '#e74c3c';
|
||||
tip.textContent = message;
|
||||
setTimeout(function () { if (tip.parentNode) tip.remove(); }, 4000);
|
||||
}
|
||||
|
||||
function showLoading() {
|
||||
box.innerHTML = '<div style="text-align:center;padding:20px;color:#9ca3af;">验证码加载中...</div>';
|
||||
}
|
||||
|
||||
function clearLoading() {
|
||||
box.innerHTML = '';
|
||||
}
|
||||
|
||||
var instance = {
|
||||
uid: C.uid,
|
||||
scene: C.scene,
|
||||
box: box,
|
||||
mount: mount,
|
||||
scope: scopeBoundary(box, C),
|
||||
runLocked: false,
|
||||
lockWaiters: [],
|
||||
collect: collectFields,
|
||||
reset: resetVerificationState,
|
||||
verify: function () {
|
||||
var challengeField = container.querySelector('input[name="aksrt_captcha_challenge"]');
|
||||
var preparation = challengeField && challengeField.value ? Promise.resolve() : refreshChallenge();
|
||||
return preparation.then(function () {
|
||||
challengeField = container.querySelector('input[name="aksrt_captcha_challenge"]');
|
||||
if (!challengeField || !challengeField.value) {
|
||||
showTip('人机验证会话未就绪,请稍后重试', '#e74c3c');
|
||||
return false;
|
||||
}
|
||||
if (verified || hField.value === '1') return true;
|
||||
return new Promise(function (resolveVerify) {
|
||||
pending.push(resolveVerify);
|
||||
if (pending.length === 1) triggerCaptcha();
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
register(instance);
|
||||
refreshingChallenge = refreshChallenge();
|
||||
|
||||
// ============ 腾讯云 Captcha 2.0 ============
|
||||
function loadTencent() {
|
||||
showLoading();
|
||||
loadScriptOnce('tencent', 'https://turing.captcha.qcloud.com/TJCaptcha.js', function () {
|
||||
return typeof TencentCaptcha !== 'undefined';
|
||||
return typeof window.TencentCaptcha !== 'undefined';
|
||||
}).then(function () {
|
||||
clearLoading();
|
||||
var capIns = null;
|
||||
var captchaInstance = null;
|
||||
triggerCaptcha = function () {
|
||||
if (!capIns) {
|
||||
capIns = new TencentCaptcha(C.tc.app_id, function (res) {
|
||||
if (res.ret === 2) { cancelPending('已取消验证'); return; }
|
||||
if (res.ret === 0 && res.ticket) {
|
||||
if (res.ticket.indexOf('trerror_') === 0) {
|
||||
showTip('验证码服务异常,请刷新页面重试', '#e67e22');
|
||||
if (!captchaInstance) {
|
||||
captchaInstance = new window.TencentCaptcha(C.tc.app_id, function (result) {
|
||||
if (result.ret === 2) { cancelPending('已取消验证'); return; }
|
||||
if (result.ret === 0 && result.ticket) {
|
||||
if (result.ticket.indexOf('trerror_') === 0) {
|
||||
cancelPending('验证码服务异常,请刷新页面重试');
|
||||
return;
|
||||
}
|
||||
setField('tencent_ticket', res.ticket);
|
||||
setField('tencent_randstr', res.randstr);
|
||||
setField('tencent_sid', res.sid || '');
|
||||
submitForm();
|
||||
setField('tencent_ticket', result.ticket);
|
||||
setField('tencent_randstr', result.randstr);
|
||||
completeProviderVerification();
|
||||
return;
|
||||
}
|
||||
if (res.errorCode) {
|
||||
var errorMap = { 1001: '验证码组件加载失败', 1002: '验证码超时', 1006: '验证码配置拉取失败', 1008: '验证码页面加载异常', 1012: '验证码刷新次数过多', 1013: '验证码网络异常' };
|
||||
showTip(errorMap[res.errorCode] || ('验证码异常(errorCode:' + res.errorCode + ')'), '#e74c3c');
|
||||
}
|
||||
var errorMap = { 1001: '验证码组件加载失败', 1002: '验证码超时', 1006: '验证码配置拉取失败', 1008: '验证码页面加载异常', 1012: '验证码刷新次数过多', 1013: '验证码网络异常' };
|
||||
cancelPending(errorMap[result.errorCode] || '验证码异常,请重试');
|
||||
}, { userLanguage: 'zh-cn' });
|
||||
}
|
||||
capIns.show();
|
||||
captchaInstance.show();
|
||||
};
|
||||
}).catch(function () { clearLoading(); showTip('验证码组件加载失败,请刷新重试', '#e74c3c'); });
|
||||
}).catch(function () {
|
||||
clearLoading();
|
||||
cancelPending('验证码组件加载失败,请刷新重试');
|
||||
});
|
||||
}
|
||||
|
||||
// ============ Cloudflare Turnstile(弹窗) ============
|
||||
function loadCloudflare() {
|
||||
var cfWidgetId = null, cfVerifying = false, cfModal = null;
|
||||
var widgetId = null;
|
||||
var verifying = false;
|
||||
var modal = null;
|
||||
|
||||
if (typeof turnstile === 'undefined') {
|
||||
window['onTurnstileLoad_' + C.uid] = function () {};
|
||||
loadScriptOnce('cloudflare', 'https://challenges.cloudflare.com/turnstile/v0/api.js?onload=onTurnstileLoad_' + C.uid + '&render=explicit', function () {
|
||||
return typeof turnstile !== 'undefined';
|
||||
});
|
||||
function closeModal() {
|
||||
if (widgetId !== null && typeof window.turnstile !== 'undefined') {
|
||||
try { window.turnstile.remove(widgetId); } catch (_) {}
|
||||
}
|
||||
widgetId = null;
|
||||
if (modal && modal.parentNode) modal.parentNode.removeChild(modal);
|
||||
modal = null;
|
||||
}
|
||||
|
||||
function renderWidget() {
|
||||
if (cfWidgetId) { try { turnstile.remove(cfWidgetId); } catch (e) {} cfWidgetId = null; }
|
||||
if (typeof turnstile === 'undefined') { setTimeout(renderWidget, 200); return; }
|
||||
cfWidgetId = turnstile.render('#modalWidget_' + C.uid, {
|
||||
sitekey: C.cf.site_key, theme: C.cf.theme, size: C.cf.size, action: 'aksrt-verify', cdata: C.scene,
|
||||
if (typeof window.turnstile === 'undefined') { setTimeout(renderWidget, 200); return; }
|
||||
widgetId = window.turnstile.render('#modalWidget_' + C.uid, {
|
||||
sitekey: C.cf.site_key,
|
||||
theme: C.cf.theme,
|
||||
size: C.cf.size,
|
||||
action: 'aksrt-verify',
|
||||
cdata: C.scene,
|
||||
callback: function (token) {
|
||||
if (!cfVerifying) return;
|
||||
cfVerifying = false;
|
||||
if (!verifying) return;
|
||||
verifying = false;
|
||||
setField('cf_turnstile_response', token);
|
||||
closeModal();
|
||||
submitForm();
|
||||
completeProviderVerification();
|
||||
},
|
||||
'error-callback': function () { showTip('验证码失败,请重试', '#e74c3c'); cfWidgetId = null; setTimeout(renderWidget, 500); },
|
||||
'expired-callback': function () { showTip('验证码已过期,请重新验证', '#e67e22'); cfWidgetId = null; setTimeout(renderWidget, 500); }
|
||||
'error-callback': function () {
|
||||
verifying = false;
|
||||
closeModal();
|
||||
cancelPending('验证码失败,请重试');
|
||||
},
|
||||
'expired-callback': function () {
|
||||
verifying = false;
|
||||
closeModal();
|
||||
cancelPending('验证码已过期,请重新验证');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function createModal() {
|
||||
cfModal = document.createElement('div');
|
||||
cfModal.className = 'aksrt-captcha-modal-overlay';
|
||||
cfModal.innerHTML = '<div class="aksrt-captcha-modal-backdrop"></div>' +
|
||||
modal = document.createElement('div');
|
||||
modal.className = 'aksrt-captcha-modal-overlay';
|
||||
modal.innerHTML = '<div class="aksrt-captcha-modal-backdrop"></div>' +
|
||||
'<div class="aksrt-captcha-modal-box"><div class="aksrt-captcha-modal-header">' +
|
||||
'<span class="aksrt-captcha-modal-title">人机验证</span>' +
|
||||
'<button type="button" class="aksrt-captcha-modal-close">×</button></div>' +
|
||||
'<div class="aksrt-captcha-modal-body"><p class="aksrt-captcha-modal-hint">请完成验证后继续操作</p>' +
|
||||
'<div id="modalWidget_' + C.uid + '" class="aksrt-captcha-modal-widget"></div></div></div></div>';
|
||||
document.body.appendChild(cfModal);
|
||||
var cancelFn = function () { cfVerifying = false; closeModal(); cancelPending('已取消验证'); };
|
||||
cfModal.querySelector('.aksrt-captcha-modal-backdrop').addEventListener('click', cancelFn);
|
||||
cfModal.querySelector('.aksrt-captcha-modal-close').addEventListener('click', cancelFn);
|
||||
document.body.appendChild(modal);
|
||||
var cancel = function () {
|
||||
verifying = false;
|
||||
closeModal();
|
||||
cancelPending('已取消验证');
|
||||
};
|
||||
modal.querySelector('.aksrt-captcha-modal-backdrop').addEventListener('click', cancel);
|
||||
modal.querySelector('.aksrt-captcha-modal-close').addEventListener('click', cancel);
|
||||
renderWidget();
|
||||
}
|
||||
|
||||
function closeModal() {
|
||||
if (cfWidgetId) { try { turnstile.remove(cfWidgetId); } catch (e) {} cfWidgetId = null; }
|
||||
if (cfModal && cfModal.parentNode) { cfModal.parentNode.removeChild(cfModal); }
|
||||
cfModal = null;
|
||||
window['onTurnstileLoad_' + C.uid] = function () {};
|
||||
if (typeof window.turnstile === 'undefined') {
|
||||
loadScriptOnce('cloudflare', 'https://challenges.cloudflare.com/turnstile/v0/api.js?onload=onTurnstileLoad_' + C.uid + '&render=explicit', function () {
|
||||
return typeof window.turnstile !== 'undefined';
|
||||
}).catch(function () { cancelPending('验证码组件加载失败,请刷新重试'); });
|
||||
}
|
||||
|
||||
triggerCaptcha = function () {
|
||||
if (cfVerifying) { showTip('验证码进行中,请稍候', '#e67e22'); return; }
|
||||
cfVerifying = true;
|
||||
if (verifying) return;
|
||||
verifying = true;
|
||||
createModal();
|
||||
};
|
||||
providerReset = closeModal;
|
||||
}
|
||||
|
||||
// ============ 极验 GeeTest v4 ============
|
||||
function loadGeetest() {
|
||||
showLoading();
|
||||
var captchaId = (C.gt && C.gt.captcha_id) ? String(C.gt.captcha_id).trim() : '';
|
||||
var product = (C.gt && C.gt.product) ? String(C.gt.product) : 'bind';
|
||||
var captchaId = C.gt && C.gt.captcha_id ? String(C.gt.captcha_id).trim() : '';
|
||||
var product = C.gt && C.gt.product ? String(C.gt.product) : 'bind';
|
||||
if (!captchaId) {
|
||||
clearLoading();
|
||||
showTip('极验 Captcha ID 未配置,请到后台「功能&权限 → 人机验证」填写并保存', '#e74c3c');
|
||||
cancelPending('极验 Captcha ID 未配置');
|
||||
return;
|
||||
}
|
||||
|
||||
function render() {
|
||||
clearLoading();
|
||||
initGeetest4({ captchaId: captchaId, product: product, hideSuccess: true }, function (capIns) {
|
||||
window['__gt_' + C.uid] = capIns;
|
||||
window.initGeetest4({ captchaId: captchaId, product: product, hideSuccess: true }, function (captchaInstance) {
|
||||
function onSuccess() {
|
||||
var result = capIns.getValidate();
|
||||
if (!result) return;
|
||||
var result = captchaInstance.getValidate();
|
||||
if (!result) { cancelPending('验证码失败,请重试'); return; }
|
||||
setField('geetest_lot_number', result.lot_number);
|
||||
setField('geetest_captcha_output', result.captcha_output);
|
||||
setField('geetest_pass_token', result.pass_token);
|
||||
setField('geetest_gen_time', result.gen_time);
|
||||
submitForm();
|
||||
}
|
||||
if (product === 'bind' || product === 'popup') {
|
||||
capIns.onReady(function () {}).onSuccess(onSuccess).onClose(function () { cancelPending('已取消验证'); });
|
||||
} else {
|
||||
capIns.appendTo('#' + C.uid).onSuccess(onSuccess).onClose(function () { cancelPending('已取消验证'); });
|
||||
completeProviderVerification();
|
||||
}
|
||||
captchaInstance.onReady(function () {}).onSuccess(onSuccess).onClose(function () { cancelPending('已取消验证'); });
|
||||
if (product !== 'bind' && product !== 'popup') captchaInstance.appendTo('#' + C.uid);
|
||||
triggerCaptcha = function () {
|
||||
if (product === 'bind' || product === 'popup') captchaInstance.showCaptcha();
|
||||
else showTip('请先完成人机验证', '#e67e22');
|
||||
};
|
||||
providerReset = function () {
|
||||
if (typeof captchaInstance.reset === 'function') captchaInstance.reset();
|
||||
};
|
||||
});
|
||||
triggerCaptcha = function () {
|
||||
var instance = window['__gt_' + C.uid];
|
||||
if (!instance) { showTip('验证码加载中,请稍候再试', '#e67e22'); return; }
|
||||
if (product === 'bind' || product === 'popup') { instance.showCaptcha(); return; }
|
||||
showTip('请先完成人机验证', '#e67e22');
|
||||
};
|
||||
}
|
||||
if (typeof initGeetest4 === 'undefined') {
|
||||
|
||||
if (typeof window.initGeetest4 === 'undefined') {
|
||||
loadScriptOnce('geetest', 'https://static.geetest.com/v4/gt4.js', function () {
|
||||
return typeof initGeetest4 !== 'undefined';
|
||||
}).then(render).catch(function () { clearLoading(); showTip('极验组件加载失败', '#e74c3c'); });
|
||||
return;
|
||||
return typeof window.initGeetest4 !== 'undefined';
|
||||
}).then(render).catch(function () {
|
||||
clearLoading();
|
||||
cancelPending('极验组件加载失败');
|
||||
});
|
||||
} else {
|
||||
render();
|
||||
}
|
||||
render();
|
||||
}
|
||||
|
||||
// ============ 阿里云 CAPTCHA 2.0 V3 ============
|
||||
function loadAliyun() {
|
||||
showLoading();
|
||||
window.AliyunCaptchaConfig = { region: C.al.region || 'cn', prefix: C.al.prefix };
|
||||
|
||||
function render() {
|
||||
clearLoading();
|
||||
var captchaIns = null;
|
||||
var btn = box.closest('form') ? box.closest('form').querySelector('button[type="submit"], input[type="submit"]') : null;
|
||||
var captchaInstance = null;
|
||||
window.initAliyunCaptcha({
|
||||
SceneId: C.al.scene_id, mode: 'popup', element: '#' + C.uid,
|
||||
button: btn ? '#' + (btn.id || (function () { btn.id = 'aksrt-captcha-btn-' + C.scene; return btn.id; })()) : null,
|
||||
success: function (captchaVerifyParam) {
|
||||
setField('aliyun_captcha_verify_param', captchaVerifyParam);
|
||||
submitForm();
|
||||
SceneId: C.al.scene_id,
|
||||
mode: 'popup',
|
||||
element: '#' + C.uid,
|
||||
success: function (verifyParam) {
|
||||
setField('aliyun_captcha_verify_param', verifyParam);
|
||||
completeProviderVerification();
|
||||
},
|
||||
fail: function (result) { showTip('验证码失败: ' + (result || '请重试'), '#e74c3c'); captchaIns = null; },
|
||||
getInstance: function (instance) { captchaIns = instance; },
|
||||
slideStyle: { width: 360, height: 40 }, language: 'cn'
|
||||
fail: function () { cancelPending('验证码失败,请重试'); },
|
||||
getInstance: function (value) { captchaInstance = value; },
|
||||
slideStyle: { width: 360, height: 40 },
|
||||
language: 'cn'
|
||||
});
|
||||
triggerCaptcha = function () {
|
||||
if (captchaIns && captchaIns.show) { captchaIns.show(); return; }
|
||||
showTip('验证码组件加载中,请稍候再试', '#e67e22');
|
||||
if (captchaInstance && captchaInstance.show) captchaInstance.show();
|
||||
else cancelPending('验证码组件加载中,请稍候再试');
|
||||
};
|
||||
providerReset = function () {
|
||||
if (captchaInstance && typeof captchaInstance.reset === 'function') captchaInstance.reset();
|
||||
};
|
||||
}
|
||||
|
||||
if (typeof window.initAliyunCaptcha === 'undefined') {
|
||||
loadScriptOnce('aliyun', 'https://o.alicdn.com/captcha-frontend/aliyunCaptcha/AliyunCaptcha.js', function () {
|
||||
return typeof window.initAliyunCaptcha !== 'undefined';
|
||||
}).then(render).catch(function () { clearLoading(); showTip('验证码组件加载失败,请刷新重试', '#e74c3c'); });
|
||||
return;
|
||||
}
|
||||
render();
|
||||
}
|
||||
|
||||
function loadProvider() {
|
||||
switch (C.provider) {
|
||||
case 'tencent': loadTencent(); break;
|
||||
case 'cloudflare': loadCloudflare(); break;
|
||||
case 'geetest': loadGeetest(); break;
|
||||
case 'aliyun': loadAliyun(); break;
|
||||
}).then(render).catch(function () {
|
||||
clearLoading();
|
||||
cancelPending('验证码组件加载失败,请刷新重试');
|
||||
});
|
||||
} else {
|
||||
render();
|
||||
}
|
||||
}
|
||||
|
||||
function init() {
|
||||
box = document.getElementById(C.uid);
|
||||
hField = document.getElementById(C.vid);
|
||||
if (!box || !hField) return;
|
||||
|
||||
var tries = 0;
|
||||
(function retryFind() {
|
||||
findForm();
|
||||
if (!form && ++tries < 10) { setTimeout(retryFind, 150); }
|
||||
else {
|
||||
if (form) bindSubmit();
|
||||
setTimeout(loadProvider, 0);
|
||||
}
|
||||
})();
|
||||
switch (C.provider) {
|
||||
case 'tencent': loadTencent(); break;
|
||||
case 'cloudflare': loadCloudflare(); break;
|
||||
case 'geetest': loadGeetest(); break;
|
||||
case 'aliyun': loadAliyun(); break;
|
||||
}
|
||||
|
||||
init();
|
||||
}
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', scan);
|
||||
} else {
|
||||
setTimeout(scan, 50);
|
||||
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', scan);
|
||||
else setTimeout(scan, 0);
|
||||
|
||||
if (window.MutationObserver) {
|
||||
new MutationObserver(function (mutations) {
|
||||
var shouldScan = mutations.some(function (mutation) { return mutation.addedNodes && mutation.addedNodes.length; });
|
||||
if (shouldScan) scan();
|
||||
}).observe(document.documentElement, { childList: true, subtree: true });
|
||||
}
|
||||
})();
|
||||
|
||||
Reference in New Issue
Block a user