310 lines
9.5 KiB
JavaScript
310 lines
9.5 KiB
JavaScript
(function (root, factory) {
|
|
// 验证码适配层同时支持浏览器运行和 Node 单元测试。
|
|
if (typeof module === 'object' && module.exports) {
|
|
module.exports = factory(root);
|
|
return;
|
|
}
|
|
|
|
root.CaptchaPages = factory(root);
|
|
})(typeof globalThis !== 'undefined' ? globalThis : window, function (root) {
|
|
'use strict';
|
|
|
|
var DEFAULT_SCENE_CODE = 'WEB_H5_LOGIN';
|
|
var CAPTCHA_LAYER_AREA = ['318px', '318px'];
|
|
var captchaLayerSeed = 0;
|
|
|
|
function getApi() {
|
|
return root.GenealogyApi && root.GenealogyApi.defaultClient;
|
|
}
|
|
|
|
function query(selector, rootNode) {
|
|
return (rootNode || root.document).querySelector(selector);
|
|
}
|
|
|
|
function showMessage(message) {
|
|
if (root.MessageUtil && root.MessageUtil.show) {
|
|
root.MessageUtil.show(message);
|
|
return;
|
|
}
|
|
|
|
if (root.console && root.console.warn) root.console.warn(message);
|
|
}
|
|
|
|
function buildChallengeBody(options, api) {
|
|
// 请求体字段严格对应 PC YAML 的 VerificationChallengeBody。
|
|
var settings = options || {};
|
|
|
|
return {
|
|
tenantId: settings.tenantId || api.tenantId,
|
|
clientId: settings.clientId || api.clientId,
|
|
sceneCode: settings.sceneCode || DEFAULT_SCENE_CODE,
|
|
subject: settings.subject || ''
|
|
};
|
|
}
|
|
|
|
function normalizeChallengeData(data) {
|
|
// TAC 需要 data.type、data.id 和图片字段;接口文档返回 captchaType、challengeId、payload。
|
|
var source = data || {};
|
|
var payload = source.payload || {};
|
|
|
|
return Object.assign({}, payload, {
|
|
type: source.captchaType || payload.type || 'SLIDER',
|
|
id: payload.id || source.challengeId,
|
|
challengeId: source.challengeId,
|
|
providerCode: source.providerCode || 'tianai',
|
|
captchaType: source.captchaType || payload.type || 'SLIDER'
|
|
});
|
|
}
|
|
|
|
function adaptChallengeResponse(response) {
|
|
var payload = response || {};
|
|
|
|
if (Number(payload.code) !== 200 || !payload.data) return payload;
|
|
|
|
return Object.assign({}, payload, {
|
|
data: normalizeChallengeData(payload.data)
|
|
});
|
|
}
|
|
|
|
function buildVerifyBody(requestData, context, api) {
|
|
// requestData 是 TAC 拖动完成后的轨迹数据;context 保存本次挑战的接口字段。
|
|
var settings = context || {};
|
|
var source = requestData || {};
|
|
|
|
return {
|
|
tenantId: settings.tenantId || api.tenantId,
|
|
clientId: settings.clientId || api.clientId,
|
|
sceneCode: settings.sceneCode || DEFAULT_SCENE_CODE,
|
|
subject: settings.subject || '',
|
|
challengeId: settings.challengeId || source.challengeId || source.id,
|
|
providerCode: settings.providerCode || 'tianai',
|
|
captchaType: settings.captchaType || 'SLIDER',
|
|
payload: {
|
|
id: source.id,
|
|
data: source.data || {}
|
|
}
|
|
};
|
|
}
|
|
|
|
function normalizeVerifyResponse(response) {
|
|
// TAC 只看 code 是否为 200;接口通过 passed 表示业务验证结果。
|
|
var payload = response || {};
|
|
|
|
if (Number(payload.code) === 200 && payload.data && payload.data.passed === false) {
|
|
return Object.assign({}, payload, {
|
|
code: 4001,
|
|
msg: payload.data.message || payload.msg || '验证失败'
|
|
});
|
|
}
|
|
|
|
return payload;
|
|
}
|
|
|
|
function extractValidToken(response) {
|
|
var payload = response || {};
|
|
var data = payload.data || {};
|
|
|
|
if (Number(payload.code) !== 200) return '';
|
|
if (data.passed === false) return '';
|
|
return data.validToken || payload.validToken || '';
|
|
}
|
|
|
|
function getTokenField(form) {
|
|
return query('input[name="validToken"]', form);
|
|
}
|
|
|
|
function createCaptchaLayer() {
|
|
// Layui 只负责提供默认弹层容器,验证码外观完全由 TAC 自带文件决定。
|
|
if (!root.layui || !root.layui.layer || !root.layui.layer.open || !root.document) {
|
|
throw new Error('缺少 layui.layer 验证码弹层依赖');
|
|
}
|
|
|
|
var layerId = 'captcha-layer-' + (++captchaLayerSeed);
|
|
var selector = '[data-captcha-layer-box="' + layerId + '"]';
|
|
var layerIndex = root.layui.layer.open({
|
|
type: 1,
|
|
title: false,
|
|
closeBtn: 0,
|
|
// 尺寸来自 TAC 原生 #tianai-captcha-parent,提前交给 Layui 计算居中位置。
|
|
area: CAPTCHA_LAYER_AREA,
|
|
offset: 'auto',
|
|
content: '<div data-captcha-layer-box="' + layerId + '"></div>'
|
|
});
|
|
var box = query(selector);
|
|
|
|
if (!box) {
|
|
root.layui.layer.close(layerIndex);
|
|
throw new Error('缺少 Layui 验证码实际挂载点');
|
|
}
|
|
|
|
return {
|
|
box: box,
|
|
layerIndex: layerIndex
|
|
};
|
|
}
|
|
|
|
function closeCaptchaLayer(captchaLayer) {
|
|
// 组件结束后清空第三方 DOM 并关闭本次创建的 Layui 层。
|
|
if (!captchaLayer) return;
|
|
|
|
if (captchaLayer.box) captchaLayer.box.innerHTML = '';
|
|
if (root.layui && root.layui.layer && root.layui.layer.close) {
|
|
root.layui.layer.close(captchaLayer.layerIndex);
|
|
}
|
|
}
|
|
|
|
function writeValidToken(form, token) {
|
|
var field = getTokenField(form);
|
|
|
|
if (field) field.value = token || '';
|
|
}
|
|
|
|
function shouldRequireCaptcha(result) {
|
|
// /captcha/require 返回 required=false 时允许直接继续业务请求。
|
|
if (!result) return true;
|
|
return result.required !== false;
|
|
}
|
|
|
|
function createTac(form, options, api, resolve, reject) {
|
|
var settings = options || {};
|
|
var captchaLayer;
|
|
var box;
|
|
var context = buildChallengeBody(settings, api);
|
|
var lastChallenge = {};
|
|
var captcha;
|
|
var config;
|
|
|
|
if (!root.TAC || !root.CaptchaConfig) {
|
|
reject(new Error('滑动验证组件未加载'));
|
|
return null;
|
|
}
|
|
|
|
try {
|
|
captchaLayer = createCaptchaLayer();
|
|
box = captchaLayer.box;
|
|
} catch (error) {
|
|
reject(error);
|
|
return null;
|
|
}
|
|
|
|
config = new root.CaptchaConfig({
|
|
bindEl: box,
|
|
requestCaptchaDataUrl: api.buildApiUrl('/captcha/challenge'),
|
|
validCaptchaUrl: api.buildApiUrl('/captcha/verify'),
|
|
requestHeaders: {
|
|
clientid: api.clientId
|
|
},
|
|
validSuccess: function (response, captchaControl, captchaInstance) {
|
|
var token = extractValidToken(response);
|
|
|
|
if (!token) {
|
|
if (captchaInstance && captchaInstance.destroyWindow) captchaInstance.destroyWindow();
|
|
closeCaptchaLayer(captchaLayer);
|
|
reject(new Error('滑动验证未返回有效票据'));
|
|
return;
|
|
}
|
|
|
|
writeValidToken(form, token);
|
|
if (captchaInstance && captchaInstance.destroyWindow) captchaInstance.destroyWindow();
|
|
closeCaptchaLayer(captchaLayer);
|
|
resolve(token);
|
|
},
|
|
validFail: function (response, captchaControl, captchaInstance) {
|
|
if (captchaInstance && captchaInstance.reloadCaptcha) captchaInstance.reloadCaptcha();
|
|
showMessage((response && (response.msg || response.message)) || '滑动验证失败,请重试');
|
|
},
|
|
btnCloseFun: function (event, captchaInstance) {
|
|
if (captchaInstance) captchaInstance.destroyWindow();
|
|
closeCaptchaLayer(captchaLayer);
|
|
reject(new Error('已取消滑动验证'));
|
|
}
|
|
});
|
|
|
|
config.addRequestChain({
|
|
preRequest: function (type, request) {
|
|
if (type === 'requestCaptchaData') {
|
|
request.data = buildChallengeBody(settings, api);
|
|
}
|
|
|
|
if (type === 'validCaptcha') {
|
|
request.data = buildVerifyBody(request.data, Object.assign({}, context, lastChallenge), api);
|
|
}
|
|
|
|
return true;
|
|
},
|
|
postRequest: function (type, request, response) {
|
|
if (type === 'requestCaptchaData') {
|
|
var adapted = adaptChallengeResponse(response);
|
|
|
|
lastChallenge = {
|
|
challengeId: adapted.data && adapted.data.challengeId,
|
|
providerCode: adapted.data && adapted.data.providerCode,
|
|
captchaType: adapted.data && adapted.data.captchaType
|
|
};
|
|
|
|
Object.keys(response || {}).forEach(function (key) {
|
|
delete response[key];
|
|
});
|
|
Object.assign(response, adapted);
|
|
}
|
|
|
|
if (type === 'validCaptcha') {
|
|
Object.assign(response, normalizeVerifyResponse(response));
|
|
}
|
|
|
|
return true;
|
|
}
|
|
});
|
|
|
|
try {
|
|
captcha = new root.TAC(config);
|
|
captcha.init();
|
|
} catch (error) {
|
|
closeCaptchaLayer(captchaLayer);
|
|
reject(error);
|
|
return null;
|
|
}
|
|
|
|
return captcha;
|
|
}
|
|
|
|
async function ensureToken(form, options) {
|
|
// 认证页提交前调用:已有 validToken 直接通过,否则按验证中心策略弹出 TAC。
|
|
var api = getApi();
|
|
var field = getTokenField(form);
|
|
var settings = options || {};
|
|
var requirement;
|
|
|
|
if (!api) return '';
|
|
if (field && field.value) return field.value;
|
|
if (!settings.sceneCode) return '';
|
|
|
|
try {
|
|
requirement = await api.captchaRequirement(buildChallengeBody(settings, api));
|
|
if (!shouldRequireCaptcha(requirement)) return 'CAPTCHA_NOT_REQUIRED';
|
|
} catch (error) {
|
|
// 查询策略失败时仍尝试直接拉起验证,避免认证接口缺少 validToken。
|
|
requirement = null;
|
|
}
|
|
|
|
return new Promise(function (resolve, reject) {
|
|
createTac(form, settings, api, resolve, reject);
|
|
}).catch(function (error) {
|
|
showMessage(error.message || '请先完成滑动验证');
|
|
return '';
|
|
});
|
|
}
|
|
|
|
return {
|
|
buildChallengeBody: buildChallengeBody,
|
|
adaptChallengeResponse: adaptChallengeResponse,
|
|
buildVerifyBody: buildVerifyBody,
|
|
createCaptchaLayer: createCaptchaLayer,
|
|
closeCaptchaLayer: closeCaptchaLayer,
|
|
extractValidToken: extractValidToken,
|
|
normalizeVerifyResponse: normalizeVerifyResponse,
|
|
shouldRequireCaptcha: shouldRequireCaptcha,
|
|
ensureToken: ensureToken
|
|
};
|
|
});
|